(async function (window) { // XMLHttpRequest 폴리필 if (!window.XMLHttpRequest) { window.XMLHttpRequest = function () { try { return new ActiveXObject('Msxml2.XMLHTTP.6.0'); } catch (e) { } try { return new ActiveXObject('Msxml2.XMLHTTP.3.0'); } catch (e) { } try { return new ActiveXObject('Msxml2.XMLHTTP'); } catch (e) { } throw new Error('이 브라우저는 XMLHttpRequest를 지원하지 않습니다.'); }; } // URLSearchParams 폴리필 if (!window.URLSearchParams) { window.URLSearchParams = function (searchString) { let searchParams = new Map(); function decodeQueryParam(param) { return decodeURIComponent(param.replace(/\+/g, ' ')); } function parseKeyValuePair(keyValue) { let pair = keyValue.split('='); let name = decodeQueryParam(pair[0]); let value = pair.length > 1 ? decodeQueryParam(pair[1]) : ''; return { name: name, value: value }; } function addToSearchParams(name, value) { if (!searchParams.has(name)) { searchParams.set(name, value); } else { let existingValue = searchParams.get(name); if (Array.isArray(existingValue)) { existingValue.push(value); } else { searchParams.set(name, [existingValue, value]); } } } searchString = searchString.replace(/^\?/, ''); let pairs = searchString.split('&'); for (let i = 0; i < pairs.length; i++) { let pair = parseKeyValuePair(pairs[i]); addToSearchParams(pair.name, pair.value); } return searchParams; }; } // Object.entries 폴리필 if (!Object.entries) { Object.entries = function (obj) { return Object.keys(obj).map(key => [key, obj[key]]); }; } // Object.fromEntries 폴리필 if (!Object.fromEntries) { Object.fromEntries = function (iterable) { return [...iterable].reduce((acc, [key, value]) => { acc[key] = value; return acc; }, {}); }; } // Object.keys 폴리필 if (!Object.keys) { Object.keys = function (obj) { let keys = []; for (let key in obj) { if (obj.hasOwnProperty(key)) { keys.push(key); } } return keys; }; } // Object.values 폴리필 if (!Object.values) { Object.values = function (obj) { if (obj === null || typeof obj === 'undefined') { throw new TypeError('Cannot convert undefined or null to object'); } return Object.keys(obj).map(key => obj[key]); }; } // Function.bind 폴리필 if (!Function.prototype.bind) { Function.prototype.bind = function (oThis) { if (typeof this !== 'function') { throw new TypeError('Function.prototype.bind - 바인딩하려는 대상이 호출 가능한 함수가 아닙니다'); } let aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, fNOP = function () { }, fBound = function () { return fToBind.apply(this instanceof fNOP && oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); }; fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; }; } // Object.assign 폴리필 if (typeof Object.assign !== 'function') { Object.defineProperty(Object, 'assign', { value: function (target, varArgs) { if (target === null || typeof target === 'undefined') { throw new TypeError('Cannot convert undefined or null to object'); } const to = Object(target); for (let index = 1; index < arguments.length; index++) { const nextSource = arguments[index]; if (nextSource !== null && typeof nextSource !== 'undefined') { for (const nextKey in nextSource) { if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { to[nextKey] = nextSource[nextKey]; } } } } return to; }, writable: true, configurable: true }); } // Promise 폴리필 if (!window.Promise) { window.Promise = function (executor) { this.executor = executor; }; window.Promise.prototype.then = function (onFulfilled, onRejected) { this.executor(onFulfilled, onRejected); }; } // async 폴리필 if (!window.async) { window.async = function (generator) { return new Promise(function (resolve, reject) { function step(gen) { let generatorResult; try { generatorResult = gen(); } catch (e) { return reject(e); } if (generatorResult.done) { return resolve(generatorResult.value); } return Promise.resolve(generatorResult.value).then( function (value) { step(function () { return gen.next(value); }); }, function (err) { step(function () { return gen.throw(err); }); } ); } return step(generator); }); }; } // navigator.sendBeacon 폴리필 if (!navigator.sendBeacon) { navigator.sendBeacon = function (url, data) { let xhr = new XMLHttpRequest(); xhr.open('POST', url, false); // false는 동기적으로 요청하는 것을 피하기 위해 xhr.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8'); xhr.send(data); return true; // 항상 성공으로 가정 }; } //new 폴리필 if (typeof Object.create !== 'function') { Object.create = function (proto) { function F() { } F.prototype = proto; return new F(); }; } // padStart 폴리필 if (!String.prototype.padStart) { String.prototype.padStart = function (targetLength, padString) { targetLength = targetLength >> 0; padString = String(typeof padString !== 'undefined' ? padString : ' '); if (this.length >= targetLength) { return String(this); } else { targetLength = targetLength - this.length; if (targetLength > padString.length) { padString += padString.reqeat(targetLength / padString.length); } return padString.slice(0, targetLength) + String(this); } }; } const MINUTE = 60; const HOUR = 60 * MINUTE; const DAY = 24 * HOUR; const YEAR = 365 * DAY; const ONSITE_Z_INDEX = 100000000; const SOCIAL_Z_INDEX = ONSITE_Z_INDEX; const BACKGROUND_Z_INDEX = ONSITE_Z_INDEX + 1; const POPUP_Z_INDEX = ONSITE_Z_INDEX + 1; const scriptParams = new URLSearchParams(new URL(document.currentScript.src).search); const MEASUREMENT_ID = scriptParams.get('id'); const DL = scriptParams.get('dl').replace(/ /g, '+'); const PAGE_RENDER_TYPE = scriptParams.get('type'); const HOSTING_CAFE24 = 'cafe24'; const HOSTING_MAKESHOP = 'makeshop'; const HOSTING_GODOMALL = 'godomall'; const HOSTING_SHOPBY = 'shopby'; const checkCSRPage = window.sb || PAGE_RENDER_TYPE === "spa"; // Cookie const SNAPA = 'snapa'; const SNAPID = 'snapid'; const CAMPAIGN_TODAY_NO_SHOW = 'snap_campaign_'; const CAMPAIGN_SESSION_SHOW = 'snap_cs_campaign_'; //sessionStorage const SDL = 'snap_sdl'; // sdl 인증 토큰 키: 새 창 공유 위해 localStorage 우선 저장 + sessionStorage 중복 갱신 const SDL_TOKEN = 'snap_token'; // LocalStorage const HOSTING = 'snap_hosting'; const SU = 'snap_su'; const SNAPUID = 'snapuid'; const REF_UTM = 'snap_ref_utm'; const NSU = 'snap_nsu'; const LAST_VIEW_ITEM = 'snap_last_view_item'; // IndexedDB const DB_DATA_STORAGE = 'DataStorage'; const DB_SESSION = 'Session'; const FRONT_URL = 'https://push.snapfit.co.kr'; const PREVIEW_URL = FRONT_URL + '/Onsite/preview'; const GENERATE_CLIENT = FRONT_URL + '/Collector/generateClientId'; const GENERATE_CLIENT_RETRY = FRONT_URL + '/Collector/generateClientId'; const ONSITE_GET = FRONT_URL + '/Onsite/getOnsite'; const COLLECT_URL = FRONT_URL + '/Collector/collect'; const REFRESH_TOKEN_URL = FRONT_URL + '/Collector/refreshToken'; const CDN_URL = `https://cdn.snapfit.co.kr`; const SETTING_CDN_URL = `${CDN_URL}/onsite_v2/stores/${MEASUREMENT_ID}/resource/setting.json`; const BASE_CDN_URL = `${FRONT_URL}/onsite_v2/template/base.html`; const PAGE_MAP = { home: 'sq_main_page', item_category: 'sq_product_list_page', item_detail: 'sq_detail_page', search: 'sq_search_page', basket: 'sq_basket_page', order: 'sq_order_page', order_complete: 'sq_order_result_page', join: 'sq_join_page', join_complete: 'sq_join_complete_page', login: 'sq_login_page', mypage: 'sq_join_page', other_page: 'sq_other_page', event: 'sq_event_page', }; const stmParams = { isMember: false, cacheType: new Set(), itemCategory: {}, itemName: {}, }; const ScrollLock = { count: 0, handler: (e) => e.preventDefault(), lock() { this.count += 1; if (this.count === 1) { window.addEventListener('wheel', this.handler, { passive: false }); window.addEventListener('touchmove', this.handler, { passive: false }); document.documentElement.style.overflow = 'hidden'; document.body.style.overflow = 'hidden'; } }, unlock() { if (this.count === 0) { return; } this.count -= 1; if (this.count === 0) { window.removeEventListener('wheel', this.handler); window.removeEventListener('touchmove', this.handler); document.documentElement.style.overflow = ''; document.body.style.overflow = ''; } }, }; function SnapTagNavigator(options) { if (window.__SNAPTAG_NAV_INSTALLED__) return; window.__SNAPTAG_NAV_INSTALLED__ = true; options = options || {}; this.timeoutMs = typeof options.timeoutMs === "number" ? options.timeoutMs : 2500; this._navToken = 0; this._runed = false; } SnapTagNavigator.prototype.run = function () { if (this._runed) return; this._runed = true; //샵바이 확인 if (window.sb) { if (sb?.profile) { const parent = document.createElement("div"); parent.style = "display: none"; parent.className = "snapInit"; const ids = ['sf_user_name', 'sf_group_name', 'sf_member_name']; const classnames = ['xans-member-var-id', 'xans-member-var-group_name', 'xans-member-var-name']; const sbdatas = ['memberNo', 'memberGradeName', 'memberName']; for (let i = 0; i < 3; i++) { const child = document.createElement("div"); child.className = classnames[i]; child.id = ids[i]; child.textContent = sb?.profile?.[sbdatas[i]]; child.style = "display: none"; parent.appendChild(child); } document.body.prepend(parent); } } stm.run(); //샵바이 여부와 상관 없이 spa인 경우 실행 if (PAGE_RENDER_TYPE === "spa") { this._wrapHistoryMethod("pushState"); this._wrapHistoryMethod("replaceState"); window.addEventListener("hashchange", () => this._handleAfterScreenChange()); window.addEventListener("popstate", () => this._handleAfterScreenChange()); } }; SnapTagNavigator.prototype.getSpaRoot = function () { return ( document.querySelector("#root") || document.querySelector("#app") || document.querySelector("main") || document.body ); }; SnapTagNavigator.prototype.waitForScreenChange = function (root, timeoutMs) { timeoutMs = typeof timeoutMs === "number" ? timeoutMs : 2000; return new Promise((resolve) => { let done = false; let timerId = null; function cleanup(mo) { if (timerId != null) { clearTimeout(timerId); timerId = null; } try { mo.disconnect(); } catch (e) { } } function finish(mo) { if (done) return; done = true; cleanup(mo); requestAnimationFrame(function () { requestAnimationFrame(resolve); }); } let mo = new MutationObserver(function () { finish(mo); }); mo.observe(root, { childList: true, subtree: true, attributes: true, characterData: true, }); timerId = setTimeout(function () { finish(mo); }, timeoutMs); }); }; SnapTagNavigator.prototype._handleAfterScreenChange = async function () { stm.reset(); let token = ++this._navToken; let root = this.getSpaRoot(); await this.waitForScreenChange(root, this.timeoutMs); // 최신 내비게이션만 반영 if (token !== this._navToken) return; try { stm.run(true); } catch (_) { } }; SnapTagNavigator.prototype._wrapHistoryMethod = function (methodName) { let orig = history[methodName]; if (!orig || orig.__SNAPTAG_WRAPPED__) return; let self = this; function wrapped() { let ret = orig.apply(this, arguments); self._handleAfterScreenChange(); return ret; } wrapped.__SNAPTAG_WRAPPED__ = true; try { history[methodName] = wrapped; } catch (_) { } }; function SnaptagManager() { this.db = new DBManager(); this.observerManager = new ObserverManager(); this.hostingService = new HostingService(); this.userCacheManager = new UserCacheManager({ db: this.db, observerManager: this.observerManager }); this.messageEventManager = new MessageEventManager({ observerManager: this.observerManager }); this.onsiteManager = new OnsiteManager({ userCacheManager: this.userCacheManager, observerManager: this.observerManager, hostingService: this.hostingService, }); this.retryQueue = new RetryQueue({ userCacheManager: this.userCacheManager, observerManager: this.observerManager, hostingService: this.hostingService, onsiteManager: this.onsiteManager, }); } SnaptagManager.prototype.run = async function (popstate = false) { let instance = this; if (popstate) { instance.retryQueue.reinit(); } else { STM_Util.storage.cookie.create(SNAPA); STM_Util.storage.cookie.create(SNAPID); instance.messageEventManager.addPostMessageListener(); dataQueue.push = function (...args) { Array.prototype.push.apply(this, args); // 기존 push 동작 유지 instance.retryQueue.enqueue(args); }; await Promise.all([ this.db.init(), ]); instance.retryQueue.init(dataQueue, popstate); } }; SnaptagManager.prototype.reset = function () { if (typeof dataQueue === 'object') { dataQueue.length = 0; } this.retryQueue.enable = false; this.onsiteManager.resetIframe(); }; const STM_Util = {}; STM_Util.security = {}; STM_Util.uuid = {}; STM_Util.date = {}; STM_Util.navigator = {}; STM_Util.url = {}; STM_Util.storage = {}; STM_Util.storage.cookie = {}; STM_Util.storage.localStorage = {}; STM_Util.hosting = {}; STM_Util.hosting.cafe24 = {}; STM_Util.condition = {}; STM_Util.convert = {}; STM_Util.customItemUrl = ''; STM_Util.shopbyAuth = { tokenKey: '', tokenJsonKey: '', clientId: '' }; STM_Util.getPageType = function () { let pageType = 'UNKOWN'; if (dataQueue === null || typeof dataQueue === 'undefined' || !Array.isArray(dataQueue)) { return pageType; } for (const args of dataQueue) { if ( args && typeof args === 'object' && args[1] === 'page_view' && args[2] && args[2].page_type !== null && typeof args[2].page_type !== 'undefined' ) { pageType = args[2].page_type; break; } } return pageType; }; STM_Util.security.sha256 = async function (input) { const encoder = new TextEncoder(); const data = encoder.encode(input); const hashBuffer = await crypto.subtle.digest("SHA-256", data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); }; STM_Util.security.toBytes = function (str) { if (window.TextEncoder) { return new TextEncoder().encode(str); } const utf8 = unescape(encodeURIComponent(str)); const arr = new Uint8Array(utf8.length); for (let i = 0; i < utf8.length; i++) { arr[i] = utf8.charCodeAt(i); } return arr; }; STM_Util.security.fnv1a32 = function (str, seed) { let h = (seed === null ? 0x811c9dc5 : seed) >>> 0; const bytes = STM_Util.security.toBytes(str); for (let i = 0; i < bytes.length; i++) { h ^= bytes[i]; h = Math.imul(h, 0x01000193) >>> 0; } return h >>> 0; }; STM_Util.uuid.generateSafeUuid = function () { let uuid = STM_Util.uuid.generateUuid(); let browserInfo = STM_Util.navigator.getAgent().browser; let deviceInfo = STM_Util.navigator.getAgent().device; let originalString = [uuid, browserInfo, deviceInfo]; let binaryString = new TextEncoder().encode(originalString.join('.')); let base64String = btoa(String.fromCharCode.apply(null, binaryString)); return base64String; }; STM_Util.uuid.generateUuid = function () { let crypto = window.crypto || window.msCrypto; if (!crypto || !crypto.getRandomValues) { throw new Error('Your browser does not support secure random number generation.'); } let buffer = new Uint8Array(16); crypto.getRandomValues(buffer); buffer[6] = (buffer[6] & 0x0f) | 0x40; buffer[8] = (buffer[8] & 0x3f) | 0x80; return Array.from(buffer).map(byte => byte.toString(16).padStart(2, '0')).join(''); }; STM_Util.date.timeToSeconds = function (time) { if (!time) { return 0; } const [hours, minutes, seconds] = time.split(':').map(Number); return hours * HOUR + minutes * MINUTE + seconds; }; STM_Util.date.getDate = function (days) { const today = new Date(); today.setDate(today.getDate() - days); const year = today.getFullYear(); const month = String(today.getMonth() + 1).padStart(2, '0'); const day = String(today.getDate()).padStart(2, '0'); const formattedDate = `${year}-${month}-${day}`; return formattedDate; }; STM_Util.date.getCurrentDateTime = function () { let now = new Date(); let year = now.getFullYear(); let month = String(now.getMonth() + 1).padStart(2, '0'); let day = String(now.getDate()).padStart(2, '0'); let hour = String(now.getHours()).padStart(2, '0'); let minute = String(now.getMinutes()).padStart(2, '0'); let seconds = String(now.getSeconds()).padStart(2, '0'); formattedDateTime = `${year}-${month}-${day} ${hour}:${minute}:${seconds}`; return formattedDateTime; }; STM_Util.date.getDiffInDay = function (startDate, endDate) { const start = new Date(startDate); const end = new Date(endDate); if (isNaN(start.getTime()) || isNaN(end.getTime())) { return 0; } // 달력 날짜 단위 비교 (시각 무시) start.setHours(0, 0, 0, 0); end.setHours(0, 0, 0, 0); const diffInMs = this.getDiffInMs(start, end); return diffInMs / (1000 * 60 * 60 * 24); }; STM_Util.date.getDiffInHour = function (startDate, endDate) { const diffInMs = this.getDiffInMs(startDate, endDate); return diffInMs / (1000 * 60 * 60); }; STM_Util.date.getDiffInMinute = function (startDate, endDate) { const diffInMs = this.getDiffInMs(startDate, endDate); return diffInMs / (1000 * 60); }; STM_Util.date.getDiffInMs = function (startDate, endDate) { let dateDiff = 0; const start = new Date(startDate); if (isNaN(start.getTime())) { return dateDiff; } const end = new Date(endDate); if (isNaN(end.getTime())) { return dateDiff; } dateDiff = end - start; return dateDiff; }; STM_Util.navigator.getAgent = function () { let userAgent = navigator.userAgent.toLowerCase(); let os = 'U'; let browser = 'U'; let device = 'U'; if (/win/i.test(userAgent)) { os = 'W'; } else if (/mac/i.test(userAgent) && !/like mac/i.test(userAgent)) { os = 'M'; } else if (/android/i.test(userAgent)) { os = 'A'; } else if (/like mac/i.test(userAgent)) { os = 'I'; } else if (/linux/i.test(userAgent)) { os = 'L'; } if (/naver/i.test(userAgent)) { browser = 'N'; } else if (/kakao/i.test(userAgent)) { browser = 'K'; } else if (/zigzag/i.test(userAgent)) { browser = 'Z'; } else if (/avely/i.test(userAgent)) { browser = 'A'; } else if (/byapps/i.test(userAgent)) { browser = 'B'; } else if (/samsungbrowser/i.test(userAgent)) { browser = 'SB'; } else if (/whale/i.test(userAgent)) { browser = 'W'; } else if (/edg/i.test(userAgent)) { browser = 'EC'; } else if (/edge/i.test(userAgent) || /edgios/i.test(userAgent)) { browser = 'E'; } else if (/chrome/i.test(userAgent) || /crios/i.test(userAgent)) { browser = 'C'; } else if (/firefox/i.test(userAgent) || /fxios/i.test(userAgent)) { browser = 'F'; } else if (/safari/i.test(userAgent)) { browser = 'S'; } else if (/opera/i.test(userAgent)) { browser = 'O'; } if (/mobile/i.test(userAgent)) { device = 'M'; } else if (/tablet|ipad/i.test(userAgent)) { device = 'TAB'; } else if (/windows|mac|linux/i.test(userAgent)) { device = 'PC'; } return { os, browser, device }; }; STM_Util.navigator.getDeviceType = function () { let device = STM_Util.navigator.getAgent().device; let type = 'pc'; if (device === 'M' || device === 'TAB') { type = 'mo'; } return type; }; STM_Util.url.getQueryParams = function () { try { let searchParams = new URLSearchParams(location.search); let queryParams = Object.fromEntries(searchParams.entries()); if (queryParams === null || typeof queryParams === 'undefined') { return {}; } return queryParams; } catch (e) { return {}; } }; STM_Util.url.getUTMParams = function () { let queryParams = STM_Util.url.getQueryParams(); if (Object.keys(queryParams).length === 0) { return {}; } let utmParams = {}; for (let key of Object.keys(queryParams)) { if ( key.startsWith('utm_') || // Google UTM key.startsWith('n_') || // Naver key === 'oquery' // Naver oquery ) { utmParams[key] = STM_Util.convert.toNFC(queryParams[key]); } } Object.assign(utmParams, STM_Util.storage.localStorage.get(REF_UTM)); STM_Util.storage.localStorage.set(REF_UTM, utmParams); return utmParams; }; STM_Util.url.isPreview = function () { let searchParams = new URLSearchParams(location.search); let viewType = searchParams.get('view_type'); return viewType === 'preview'; }; STM_Util.storage.cookie.create = function (name, value, expires) { if (typeof name !== 'string' || name.trim() === '') { return; } if (name === SNAPA) { value = STM_Util.storage.cookie.get(SNAPA); expires = 2 * YEAR; if (!value) { value = STM_Util.uuid.generateSafeUuid(); } } if (name === SNAPID) { value = STM_Util.storage.cookie.get(SNAPID); expires = 30 * MINUTE; if (!value) { value = STM_Util.uuid.generateSafeUuid(); STM_Util.storage.localStorage.remove(REF_UTM); } } if (name.startsWith(CAMPAIGN_TODAY_NO_SHOW)) { const now = new Date(); const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0); expires = Math.floor((tomorrow.getTime() - now.getTime()) / 1000); } if (value === null || typeof value === 'undefined' || value === '') { return; } if (typeof expires !== 'number' || expires < 0) { return; } let cookieString = `${name}=${value}; max-age=${expires}; path=/; Secure; SameSite=None`; document.cookie = cookieString; }; STM_Util.storage.cookie.resetExpires = function (name, expires) { if (typeof name !== 'string' || name.trim() === '') { return; } if (typeof expires !== 'number' || expires < 0) { return; } if (name === SNAPID) { expires = 30 * MINUTE; } let value = STM_Util.storage.cookie.get(name); if (value === null || typeof value === 'undefined' || value === '') { return; } STM_Util.storage.cookie.create(name, value, expires); }; STM_Util.storage.cookie.get = function (name) { let cookie = `; ${document.cookie}`; let value = cookie.split(`; ${name}=`); if (value.length === 2) { return value[1].split(';')[0]; } return null; }; STM_Util.storage.localStorage.set = function (key, value) { if (key === null || typeof key === 'undefined' || value === null || typeof value === 'undefined') { return; } try { let storeValue = value; if (typeof value === 'object') { storeValue = JSON.stringify(value); } localStorage.setItem(key, storeValue); } catch (e) { return; } }; STM_Util.storage.localStorage.get = function (key) { try { let value = localStorage.getItem(key); return JSON.parse(value); } catch (e) { if (e instanceof SyntaxError) { return localStorage.getItem(key); } return null; } }; STM_Util.storage.localStorage.remove = function (key) { localStorage.removeItem(key); }; // sdl 인증 토큰: 새 창/탭 공유를 위해 localStorage에 저장(기존 localStorage 래퍼 재사용) + sessionStorage 중복 갱신. // 읽기는 localStorage 우선 → 없을 때만 sessionStorage 폴백. STM_Util.storage.sdl = {}; STM_Util.storage.sdl.set = function (value) { if (value === null || typeof value === 'undefined') { value = ''; } STM_Util.storage.localStorage.set(SDL_TOKEN, value); try { sessionStorage.setItem(SDL_TOKEN, value); } catch (e) { } }; STM_Util.storage.sdl.get = function () { let value = STM_Util.storage.localStorage.get(SDL_TOKEN); if (value === null || typeof value === 'undefined') { value = sessionStorage.getItem(SDL_TOKEN); } return value; }; STM_Util.hosting.getHosting = function () { const hosting = STM_Util.storage.localStorage.get(HOSTING); if (hosting) { return hosting; } if (window.CAFE24) { return HOSTING_CAFE24; } if (window.sb) { return HOSTING_SHOPBY; } let solution = ''; if (window.$) { solution = $('#solutiontype').text(); } else if (document.querySelector("#solutiontype")) { solution = document.querySelector("#solutiontype").textContent; } if (solution === HOSTING_MAKESHOP) { return HOSTING_MAKESHOP; } return HOSTING_GODOMALL; }; STM_Util.hosting.getUserId = function () { let loginId = ''; const user_id_selector = document.getElementsByClassName('xans-member-var-id'); if (user_id_selector && user_id_selector.length > 0) { loginId = user_id_selector[0].innerText || user_id_selector[0].textContent; } if (!loginId) { sessionStorage.setItem(SDL_TOKEN, ''); } return loginId; }; STM_Util.hosting.customItemUrlToRegex = function (path) { if (!path) { return null; } const pattern = path.replace(/\{([^}]+)\}/, "(?\\d+)"); const basePrefix = "http[s]?:\\/\\/[^\\/]+(?:\\/[^\\/]+)*"; const finalString = `${basePrefix}${pattern.replace(/\//g, "\\/")}`; const transformRegex = new RegExp(finalString, 'i'); return transformRegex; }; STM_Util.hosting.getProductData = function (url) { let regexPatterns = { cafe24: [ /http[s]?:\/\/[^\/]+(?:\/[^\/]+)*\/product\/(?[^\/]+)\/(?\d+)(?:\/category\/(?\d+))?/i, // productNameNumber /http[s]?:\/\/[^\/]+(?:\/[^\/]+)*\/product\/detail\.html\?product_no=(?\d+)(?:.*?cate_no=(?\d+))?/i, // productNo ], makeshop: [ /http[s]?:\/\/[^\/]+(?:\/[^\/]+)*\/shop\/shopdetail\.html\?branduid=(?[^&]+)(?:.*?xcode=(?[^&]+))?/i, /http[s]?:\/\/[^\/]+(?:\/[^\/]+)*\/m\/product\.html\?branduid=(?[^&]+)(?:.*?xcode=(?[^&]+))?/i, // mobile ], godomall: [ /http[s]?:\/\/[^\/]+(?:\/[^\/]+)*\/goods\/goods_view\.php\?goodsNo=(?\d+)/i, ], shopby: [ /http[s]?:\/\/[^\/]+(?:\/[^\/]+)*\/product-detail\?productNo=(?\d+)/i, /http[s]?:\/\/[^\/]+(?:\/[^\/]+)*\/pages\/product\/product-detail\.html\?productNo=(?\d+)/i, ] }; if (STM_Util.hosting.getHosting() === 'shopby' && STM_Util.customItemUrl) { const customRegex = STM_Util.hosting.customItemUrlToRegex(STM_Util.customItemUrl); if (customRegex) { regexPatterns['shopby'].push(customRegex); } } let hostingRegex = regexPatterns[STM_Util.hosting.getHosting()]; let matched = hostingRegex.map((regex) => { return url.match(regex); }).find(Boolean); if (!matched) { return {}; } let { productName, productId, categoryNumber } = matched.groups; return { type: 'product', productName: decodeURIComponent(productName), productId, categoryNumber: categoryNumber || '', }; }; STM_Util.hosting.getCategoryData = function (url) { let regexPatterns = { cafe24: [ /http[s]?:\/\/[^\/]+(?:\/[^\/]+)*\/category\/(?[^\/]+)\/(?\d+)\/?/i, /http[s]?:\/\/[^\/]+(?:\/[^\/]+)*\/product\/list_thumb\.html\?cate_no=(?\d+)/i, /http[s]?:\/\/[^\/]+(?:\/[^\/]+)*\/product\/list\.html\?cate_no=(?\d+)/i, ], makeshop: [ /http[s]?:\/\/[^\/]+(?:\/[^\/]+)*\/shop\/shopbrand\.html\?(?:[^&]*&)?xcode=(?[^&]+)(?:&[^&]*)?/i, /http[s]?:\/\/[^\/]+(?:\/[^\/]+)*\/m\/product_list\.html\?(?:[^&]*&)?xcode=(?[^&]+)(?:&[^&]*)?/i, // mobile ], godomall: [ /http[s]?:\/\/[^\/]+(?:\/[^\/]+)*\/goods\/goods_list\.php\?cateCd=(?\d+)/i, ], shopby: [ /http[s]?:\/\/[^\/]+(?:\/[^\/]+)*\/products\?categoryNo=(?\d+)/i, /http[s]?:\/\/[^\/]+(?:\/[^\/]+)*\/pages\/product\/product-list\.html\?categoryNo=(?\d+)/i, ] }; try { let hostingRegex = regexPatterns[STM_Util.hosting.getHosting()]; let matched = hostingRegex.map((regex) => { return url.match(regex); }).find(Boolean); if (!matched) { return {}; } let { categoryName, categoryNumber } = matched.groups; return { type: 'category', categoryName: categoryName ? decodeURIComponent(categoryName) : '', categoryNumber: categoryNumber || '', }; } catch (e) { return {}; } }; STM_Util.hosting.getOrderDate = function (orderId) { if (typeof orderId !== 'string' || orderId.length < 8) { return null; } let hosting = STM_Util.hosting.getHosting(); switch (hosting) { default: // 독립몰 case HOSTING_CAFE24: // 20250516-0000012 case HOSTING_MAKESHOP: // 20250516142815-63695955859 case HOSTING_SHOPBY: { // 202601071446551688 const year = orderId.slice(0, 4); const month = orderId.slice(4, 6); const day = orderId.slice(6, 8); return `${year}-${month}-${day}`; } case HOSTING_GODOMALL: { // 2505161430000001 const year = orderId.slice(0, 2); const month = orderId.slice(2, 4); const day = orderId.slice(4, 6); return `20${year}-${month}-${day}`; } } }; STM_Util.hosting.getProductLink = function (productId) { let url = '//' + location.host; switch (STM_Util.hosting.getHosting()) { case HOSTING_CAFE24: url += `/product/*/${productId}/`; break; case HOSTING_MAKESHOP: url += `/shop/shopdetail.html?branduid=${productId}`; break; case HOSTING_GODOMALL: url += `/goods/goods_view.php?goodsNo=${productId}`; break; case HOSTING_SHOPBY: if (STM_Util.customItemUrl) { url += STM_Util.customItemUrl.replace('{product_no}', productId); } else { if (PAGE_RENDER_TYPE === 'spa') { url += `/product-detail?productNo=${productId}`; } else { url += `/pages/product/product-detail.html?productNo=${productId}`; } } break; // todo: 독립몰 별도 처리 필요 default: break; } return url; }; STM_Util.hosting.cafe24.getOrder = function () { const shopNo = CAFE24API.SHOP_NO; const orderId = CAFE24.FRONT_EXTERNAL_SCRIPT_VARIABLE_DATA.order_id; return CAPP_ASYNC_METHODS.OrderDetailInfo.getAsyncData(shopNo, orderId) .then((response) => { const orderDetailInfo = response[0]; if (orderDetailInfo === null || typeof orderDetailInfo === 'undefined') { return []; } return STM_Util.convert.parsePrice(orderDetailInfo); }) .catch((error) => { console.error(error); return []; }); }; STM_Util.condition.evaluateGroup = function (conditions, operators) { if (!Array.isArray(conditions) || conditions.length === 0) { return true; } let orGroup = []; let andGroup = []; for (let i = 0; i < conditions.length; i++) { let result = false; if (typeof conditions[i].evaluate === 'function') { result = conditions[i].evaluate(); } else { result = Boolean(conditions[i]); } andGroup.push(result); const operator = operators[i].toLowerCase(); if (operator !== 'and') { orGroup.push(andGroup.every(Boolean)); andGroup = []; } } if (andGroup.length > 0) { orGroup.push(andGroup.every(Boolean)); } return orGroup.some(Boolean); }; // mac/iOS는 한글을 NFD(분해형)로 보내 'ㅅㅏㄴ'처럼 자모가 분리됨 → NFC(완성형)로 재결합 STM_Util.convert.toNFC = function (value) { if (typeof value !== 'string') { return value; } try { if (typeof value.normalize === 'function') { return value.normalize('NFC'); } } catch (e) { return value; } return value; }; STM_Util.convert.parseBoolean = function (value) { if (Array.isArray(value)) { return value.map(STM_Util.convert.parseBoolean); } if (value && typeof value === 'object') { return Object.fromEntries( Object.entries(value).map(([k, v]) => [k, STM_Util.convert.parseBoolean(v)]) ); } if (typeof value === 'string') { const s = value.trim().toLowerCase(); if (s === 'true') { return true; } if (s === 'false') { return false; } } return value; }; STM_Util.convert.parsePrice = function (value) { if (Array.isArray(value)) { return value.map(STM_Util.convert.parsePrice); } if (value && typeof value === 'object') { return Object.fromEntries( Object.entries(value).map(([k, v]) => [k, STM_Util.convert.parsePrice(v)]) ); } if (typeof value === 'string' && /^-?\d+(\.\d+)?$/.test(value.trim())) { return parseFloat(value); } return value; }; function DBManager() { this.dbName = 'SnapDB'; this.version = 2; this.db = null; this.stores = { [DB_DATA_STORAGE]: { keyPath: 'su' }, [DB_SESSION]: { keyPath: 'su' } }; this.lock = Promise.resolve(); } DBManager.prototype.init = async function () { await this.openDB(); await this.deleteExpiredStore(); }; DBManager.prototype.openDB = function () { return new Promise((resolve, reject) => { let instance = this; let database = indexedDB.open(this.dbName, this.version); database.onupgradeneeded = (event) => { let database = event.target.result; for (let storeName of Object.keys(this.stores)) { let keyPath = this.stores[storeName]; if (!database.objectStoreNames.contains(storeName)) { database.createObjectStore(storeName, keyPath); } } }; database.onsuccess = (event) => { instance.db = event.target.result; resolve(event.target.result); }; database.onerror = (event) => { reject(event.target.errorCode); }; }); }; DBManager.prototype.deleteExpiredStore = async function () { let expiredDataList = []; for (let storeName of Object.keys(this.stores)) { let storedData = await this.selectAll(storeName); let storeKeyPath = this.stores[storeName].keyPath; storedData.forEach((data) => { if (!this.isExpired(data)) { return; } let key = Array.isArray(storeKeyPath) ? storeKeyPath.map((key) => { return data[key]; }) : data[storeKeyPath]; let expiredData = { database: storeName, key: key, }; expiredDataList.push(expiredData); }); } for (let expiredData of expiredDataList) { this.delete(expiredData.database, expiredData.key); } }; DBManager.prototype.isExpired = function (data) { if (!data || !data.hasOwnProperty('expires_date')) { return true; } return data.expires_date < Date.now(); }; DBManager.prototype.update = function (database, data) { return new Promise((resolve, reject) => { const transaction = this.db.transaction(database, 'readwrite'); const store = transaction.objectStore(database); let putData = data; if (database === DB_DATA_STORAGE) { putData = { su: STM_Util.storage.localStorage.get(SNAPUID) || STM_Util.storage.cookie.get(SNAPA), ...data }; } if (database === DB_SESSION) { putData = { su: STM_Util.storage.cookie.get(SNAPID), ...data }; } putData.expires_date = Date.now() + (180 * DAY * 1000); store.put(putData); transaction.oncomplete = () => { resolve(true); }; transaction.onerror = () => { reject(transaction.error); }; }); }; DBManager.prototype.selectAll = function (database) { return new Promise((resolve, reject) => { let transaction = this.db.transaction(database, 'readonly'); let store = transaction.objectStore(database); let request = store.getAll(); request.onsuccess = (event) => { resolve(event.target.result); }; request.onerror = (event) => { reject(event.target.errorCode); }; }); }; DBManager.prototype.select = function (database, key) { return new Promise((resolve, reject) => { let transaction = this.db.transaction(database, 'readonly'); let store = transaction.objectStore(database); if (database === DB_DATA_STORAGE && !key) { key = STM_Util.storage.localStorage.get(SNAPUID) || STM_Util.storage.cookie.get(SNAPA); } if (database === DB_SESSION) { key = STM_Util.storage.cookie.get(SNAPID); } let request = store.get(key); request.onsuccess = (event) => { resolve(event.target.result); }; request.onerror = (event) => { reject(event.target.errorCode); }; }); }; DBManager.prototype.delete = function (database, key) { return new Promise((resolve, reject) => { let transaction = this.db.transaction(database, 'readwrite'); let store = transaction.objectStore(database); let request = store.delete(key); request.onsuccess = () => { resolve(); }; request.onerror = (event) => { reject(event.target.error); }; }); }; DBManager.prototype.requestLock = async function () { let releaseLock; let nextLock = new Promise((resolve) => (releaseLock = resolve)); let prevLock = this.lock; this.lock = nextLock; await prevLock; return releaseLock; }; function STM_BaseEntity({ db }) { this.db = db; this.dbName = ''; this.data = {}; } STM_BaseEntity.prototype.init = async function () { try { let result = await this.db.select(this.dbName); this.data = result || {}; } catch (e) { this.data = {}; } }; STM_BaseEntity.prototype.get = function () { return this.data; }; STM_BaseEntity.prototype.set = async function (data) { this.data = data; await this.db.update(this.dbName, data); }; STM_BaseEntity.prototype.update = async function () { }; function HostingApiAdapter() { } HostingApiAdapter.prototype.getCart = async function () { return []; }; HostingApiAdapter.prototype.addCart = async function () { return false; }; HostingApiAdapter.prototype.buyNow = async function () { }; HostingApiAdapter.prototype._fetchDocument = async function (url) { const response = await fetch(url, { credentials: 'include' }); return new DOMParser().parseFromString(await response.text(), 'text/html'); }; function Cafe24ApiAdapter() { HostingApiAdapter.call(this); } Cafe24ApiAdapter.prototype = Object.create(HostingApiAdapter.prototype); Cafe24ApiAdapter.prototype.constructor = Cafe24ApiAdapter; Cafe24ApiAdapter.prototype.getCart = async function () { if (!CAPP_ASYNC_METHODS?.BasketProduct?.getData) { console.warn('missing CAPP_ASYNC_METHODS'); return []; } const cart = []; const response = await CAPP_ASYNC_METHODS.BasketProduct.getData(); response.forEach((item) => { const item_id = item.product_no; const count = item.quantity; const sale_price = Number(item.product_price); cart.push({ item_id, count, sale_price }); }); return cart; }; Cafe24ApiAdapter.prototype.addCart = async function (params) { const data = await this._postBasket(params.itemId, params.options ?? [], { quantity_override_flag: 'F' }); if (data.result === 0) { return true; } this._handleFailResponse(data); }; Cafe24ApiAdapter.prototype.buyNow = async function (params) { const data = await this._postBasket(params.itemId, params.options ?? [], { quantity_override_flag: 'T', redirect: '1' }); if (data.result === 0) { const returnUrl = '/order/orderform.html?basket_type=A0000&delvtype=A'; if (data.isLogin === 'F') { location.href = '/member/login.html?noMember=1&returnUrl=' + encodeURIComponent(returnUrl) + '&delvtype=A'; } else { location.href = returnUrl; } } this._handleFailResponse(data); }; Cafe24ApiAdapter.prototype._postBasket = async function (itemId, options, extraFields) { const formData = new FormData(); options.filter((o) => { return o.optionData; }) .forEach((o) => { const variantCode = o.optionData.variant_code; const quantity = o.quantity || 1; formData.append('selected_item[]', `${quantity}||${variantCode}`); }); formData.append('product_no', itemId); formData.append('basket_type', 'A0000'); formData.append('delvType', 'A'); formData.append('is_direct_buy', 'F'); formData.append('command', 'add'); for (const key in extraFields) { formData.append(key, extraFields[key]); } const res = await fetch('/exec/front/order/basket/', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest', }, credentials: 'include', body: formData, }); return res.json(); }; Cafe24ApiAdapter.prototype._handleFailResponse = function (response) { if (response.alertMSG) { alert(response.alertMSG); } }; function MakeshopApiAdapter() { HostingApiAdapter.call(this); this._cache = {}; } MakeshopApiAdapter.prototype = Object.create(HostingApiAdapter.prototype); MakeshopApiAdapter.prototype.constructor = MakeshopApiAdapter; MakeshopApiAdapter.prototype.getCart = async function () { const cart = []; const doc = await this._fetchDocument('/shop/basket.html'); const tbody = doc.querySelector('form[name="forms"]')?.closest('tbody'); let rows; if (tbody) { const branduidEls = tbody.querySelectorAll('input[name=branduid]'); const amountEls = tbody.querySelectorAll('input[name=orgamount]'); rows = Array.from(branduidEls).map((branduidEl, i) => ({ branduidEl, amountEl: amountEls[i] })); } else { rows = Array.from(doc.querySelectorAll('form[name="forms"]')).map((form) => ({ branduidEl: form.querySelector('input[name=branduid]'), amountEl: form.querySelector('input[name=orgamount]'), })); } rows.forEach(({ branduidEl, amountEl }) => { const item_id = branduidEl?.value; if (!item_id) { return; } const count = Number(amountEl?.value) || 0; const priceInput = doc.querySelector(`input[name="snap_push_product_price[${item_id}]"]`); const totalPrice = priceInput ? Number(priceInput.value.replace(',', '')) : 0; const sale_price = count > 0 ? Math.floor(totalPrice / count) : 0; cart.push({ item_id, count, sale_price }); }); return cart; }; MakeshopApiAdapter.prototype.addCart = async function (params) { const data = await this._fetchItemDetail(params.itemId); const urlParams = this._buildParams(data.formData, { ordertype: '|parent.|layer', options: params.options, }); const response = await this._fetchBasketAction(urlParams); if (response.status) { return true; } this._handleFailResponse(response); }; MakeshopApiAdapter.prototype.buyNow = async function (params) { const data = await this._fetchItemDetail(params.itemId); const urlParams = this._buildParams(data.formData, { ordertype: 'baro|parent.|layer|parent.|layer', typep: 'Y', options: params.options, }); const response = await this._fetchBasketAction(urlParams); if (!response.status) { this._handleFailResponse(response); return; } let redirectUrl = '/shop/order.html'; if (STM_Util.navigator.getDeviceType() === 'pc' && !response.is_login) { redirectUrl = '/shop/qmember.html'; } location.href = redirectUrl; }; MakeshopApiAdapter.prototype._fetchItemDetail = async function (itemId) { if (this._cache[itemId]) { return this._cache[itemId]; } try { const doc = await this._fetchDocument('/shop/shopdetail.html?branduid=' + itemId); const data = { formData: this._parseFormData(doc) }; this._cache[itemId] = data; return data; } catch { return { formData: {} }; } }; MakeshopApiAdapter.prototype._parseFormData = function (doc) { const formData = {}; const form = doc.querySelector('form[name="form1"]'); if (form) { form.querySelectorAll('input[type="hidden"]').forEach((input) => { if (input.name) { formData[input.name] = input.value; } }); } return formData; }; MakeshopApiAdapter.prototype._buildParams = function (formData, params) { const urlParams = new URLSearchParams(formData); urlParams.set('ordertype', params.ordertype); if (params.typep) { urlParams.set('typep', params.typep); } (params.options || []).forEach((option, i) => { const { optionData, quantity } = option; urlParams.append('amount[]', quantity); (optionData.opt_list || []).forEach((opt, j) => { urlParams.set(`option[basic][${i}][${j}][opt_id]`, opt.opt_id); urlParams.set(`option[basic][${i}][${j}][opt_value]`, opt.opt_value); urlParams.set(`option[basic][${i}][${j}][opt_stock]`, quantity); urlParams.set(`option[basic][${i}][${j}][sto_id]`, opt.sto_id); urlParams.set(`option[basic][${i}][${j}][opt_type]`, opt.opt_type); }); }); return urlParams; }; MakeshopApiAdapter.prototype._fetchBasketAction = async function (urlParams) { const prefix = STM_Util.navigator.getDeviceType() === 'pc' ? 'shop' : 'm'; const response = await fetch(`/${prefix}/basket.action.html`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', }, credentials: 'include', body: urlParams.toString(), }); return response.json(); }; MakeshopApiAdapter.prototype._handleFailResponse = function (response) { if (response.confirm_url) { if (confirm(response.message)) { location.href = location_dir + response.confirm_url; } } else { alert(response.message); } }; function GodomallApiAdapter() { HostingApiAdapter.call(this); } GodomallApiAdapter.prototype = Object.create(HostingApiAdapter.prototype); GodomallApiAdapter.prototype.constructor = GodomallApiAdapter; GodomallApiAdapter.prototype.getCart = async function () { const cart = []; const doc = await this._fetchDocument('/order/cart.php'); const cartSnoEls = doc.querySelectorAll('input[name="cartSno[]"]'); cartSnoEls.forEach((el) => { const data = el.dataset; const item_id = data.goodsNo; const count = Number(data.defaultGoodsCnt); const totalPrice = Number(data.price); const sale_price = count > 0 ? Math.floor(totalPrice / count) : 0; cart.push({ item_id, count, sale_price }); }); return cart; }; GodomallApiAdapter.prototype.addCart = async function (params) { const body = this._buildCartBody(params, {}); const response = await fetch('/order/cart_ps.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', }, credentials: 'include', body: body.toString(), }); const result = await response.json(); if (result && result.error === 1) { return false; } return true; }; GodomallApiAdapter.prototype.buyNow = function (params) { const body = this._buildCartBody(params, { cartMode: 'd' }); const form = document.createElement('form'); form.method = 'POST'; form.action = '/order/cart_ps.php'; form.style.display = 'none'; for (const [key, value] of body.entries()) { const input = document.createElement('input'); input.type = 'hidden'; input.name = key; input.value = value; form.appendChild(input); } document.body.appendChild(form); form.submit(); }; GodomallApiAdapter.prototype._buildCartBody = function (params, extra) { const body = new URLSearchParams(); body.set('mode', 'cartIn'); for (const key in extra) { body.set(key, extra[key]); } for (let i = 0; i < params.options.length; i++) { const { optionData, quantity } = params.options[i]; body.append('goodsNo[]', String(params.itemId)); body.append('optionSno[]', String(optionData?.optionSno || '')); body.append('goodsCnt[]', String(quantity || 1)); } return body; }; function ShopbyApiAdapter() { HostingApiAdapter.call(this); } ShopbyApiAdapter.prototype = Object.create(HostingApiAdapter.prototype); ShopbyApiAdapter.prototype.constructor = ShopbyApiAdapter; ShopbyApiAdapter.prototype.getCart = async function () { const auth = await this._getAuth(); if (!auth?.accessToken) { try { const stored = localStorage.getItem('GUEST_CART') || '[]'; const data = JSON.parse(stored); const cart = (Array.isArray(data) ? data : []).map((item) => { const item_id = item?.productNo; const count = item?.orderCnt; const sale_price = item?.buyPrice; return { item_id, count, sale_price }; }); return cart; } catch (e) { console.error(e); return []; } } const response = await fetch('https://shop-api.e-ncp.com/cart', { method: 'GET', headers: this._buildHeaders(auth), credentials: 'omit', }); if (!response.ok) { return []; } let data; try { data = await response.json(); } catch (e) { return []; } const cart = []; (data?.deliveryGroups ?? []).forEach((deliveryGroup) => { (deliveryGroup?.orderProducts ?? []).forEach((orderProduct) => { (orderProduct?.orderProductOptions ?? []).forEach((item) => { const item_id = item?.productNo; const count = item?.orderCnt; const totalPrice = item?.price?.buyAmt; const sale_price = count > 0 ? Math.floor(totalPrice / count) : 0; cart.push({ item_id, count, sale_price }); }); }); }); return cart; }; ShopbyApiAdapter.prototype.addCart = async function (params) { const auth = await this._getAuth(); const products = this._buildProducts(params); if (!auth.accessToken) { const stored = JSON.parse(localStorage.getItem('GUEST_CART') || '[]'); const startNo = stored.length; const expireDate = Math.floor(Date.now() / 1000) + 604800; const items = products.map((p, i) => ({ ...p, cartNo: startNo + i + 1, expireDate })); localStorage.setItem('GUEST_CART', JSON.stringify(stored.concat(items))); return true; } const response = await fetch('https://shop-api.e-ncp.com/cart', { method: 'POST', headers: this._buildHeaders(auth), credentials: 'omit', body: JSON.stringify(products), }); if (response.ok) { return true; } const data = await response.json(); this._handleFailResponse(data); }; ShopbyApiAdapter.prototype.buyNow = async function (params) { const auth = await this._getAuth(); const products = this._buildProducts(params); const response = await fetch('https://shop-api.e-ncp.com/order-sheets', { method: 'POST', headers: this._buildHeaders(auth), credentials: 'omit', body: JSON.stringify({ cartNos: null, channelType: null, productCoupons: null, products: products, trackingKey: null, }), }); const data = await response.json(); if (response.ok && data.orderSheetNo) { location.href = '/order/' + data.orderSheetNo; } this._handleFailResponse(data); }; ShopbyApiAdapter.prototype._getAccessToken = function () { try { const key = STM_Util.shopbyAuth.tokenKey; const jsonKey = STM_Util.shopbyAuth.tokenJsonKey || 'accessToken'; // tokenKey만 등록된 상점은 값이 JSON이면 accessToken, 아니면 값 자체를 토큰으로 사용 const useRawValue = STM_Util.shopbyAuth.tokenKey && !STM_Util.shopbyAuth.tokenJsonKey; const extractToken = (raw) => { //jsonKey, useRawValue 클로저 캡쳐 if (!raw) { return null; } try { return JSON.parse(raw)?.[jsonKey] ?? null; } catch (e) { return useRawValue ? raw : null; } }; const token = extractToken(localStorage.getItem(key)); if (token) { return token; } //accessToken을 혹시 쿠키에 넣을 경우 대비 const cookieToken = extractToken(decodeURIComponent(STM_Util.storage.cookie.get(key) ?? '{}')); if (cookieToken) { return cookieToken; } const matched = document.cookie.match(/(?:^|;\s*)SHOPBY_SSID=([^;]*)/); return matched?.[1] ?? null; } catch (e) { return null; } }; ShopbyApiAdapter.prototype._getAuth = async function () { if (!STM_Util.shopbyAuth.clientId && !window.skinEnvironment?.clientId) { try { const res = await fetch('/environment.json'); window.skinEnvironment = await res.json(); } catch (e) { } } const accessToken = this._getAccessToken(); return { clientId: STM_Util.shopbyAuth.clientId || window.skinEnvironment?.clientId || '', accessToken: accessToken, }; }; ShopbyApiAdapter.prototype._buildHeaders = function (auth) { return { 'Content-Type': 'application/json', 'clientid': auth.clientId, 'shop-by-authorization': auth.accessToken ? 'Bearer ' + auth.accessToken : '', 'platform': STM_Util.navigator.getDeviceType() === 'pc' ? 'PC' : 'MOBILE_WEB', 'version': '1.0', }; }; ShopbyApiAdapter.prototype._buildProducts = function (params) { return params.options.map((opt) => { const qty = Number(opt.quantity || 1); return { ...opt.optionData, productNo: Number(params.itemId), count: qty, orderCnt: qty, optionInputs: opt.optionData?.optionInputs || [], channelType: null, additionalProductNo: 0, optionName: opt.optionData?.label || '', optionValue: opt.optionData?.value || '', saleStatus: opt.optionData?.saleType || '', calcPrice: (opt.optionData?.buyPrice || 0) * qty, }; }); }; ShopbyApiAdapter.prototype._handleFailResponse = function (response) { if (response.message) { alert(response.message); } }; function HostingService() { this.adapter = new HostingApiAdapter(); } HostingService.prototype.setup = function (config) { const hosting = config?.hosting; switch (hosting) { case HOSTING_CAFE24: this.adapter = new Cafe24ApiAdapter(); break; case HOSTING_MAKESHOP: this.adapter = new MakeshopApiAdapter(); break; case HOSTING_GODOMALL: this.adapter = new GodomallApiAdapter(); break; case HOSTING_SHOPBY: this.adapter = new ShopbyApiAdapter(); break; default: this.adapter = new HostingApiAdapter(); break; } }; HostingService.prototype.getCart = async function () { return this.adapter.getCart(); }; HostingService.prototype.addCart = async function (data) { return this.adapter.addCart(data); }; HostingService.prototype.buyNow = async function (data) { return this.adapter.buyNow(data); }; function ObserverManager() { this.observers = []; } ObserverManager.prototype.subscribe = function (observers) { if (!Array.isArray(observers)) { observers = [observers]; } this.observers.push(...observers); }; ObserverManager.prototype.notify = function (type, data) { this.observers.forEach((observer) => { if (typeof observer === 'function') { observer(type, data); } }); }; MessageEventManager.EVENT = 'POST_MESSAGE_EVENT'; MessageEventManager.EVENT_PREVIEW = 'POST_MESSAGE_PREVIEW'; MessageEventManager.EVENT_RENDER_READY = 'POST_MESSAGE_RENDER_READY'; MessageEventManager.EVENT_CLICK = 'POST_MESSAGE_CLICK'; MessageEventManager.EVENT_LINK = 'POST_MESSAGE_LINK'; MessageEventManager.EVENT_SIZE = 'POST_MESSAGE_SIZE'; MessageEventManager.EVENT_CLOSE = 'POST_MESSAGE_CLOSE'; MessageEventManager.EVENT_TODAY_NO_SHOW = 'POST_MESSAGE_TODAY_NO_SHOW'; MessageEventManager.EVENT_TRIGGER = 'POST_MESSAGE_TRIGGER'; MessageEventManager.EVENT_ADD_CART = 'POST_MESSAGE_ADD_CART'; MessageEventManager.EVENT_BUY_NOW = 'POST_MESSAGE_BUY_NOW'; MessageEventManager.EVENT_TOAST = 'POST_MESSAGE_TOAST'; MessageEventManager.EVENT_PAGE_VIEW = 'POST_MESSAGE_PAGE_VIEW'; MessageEventManager.EVENT_REFRESH_TOKEN = 'POST_MESSAGE_REFRESH_TOKEN'; function MessageEventManager({ observerManager }) { /** @type {ObserverManager} */ this.observerManager = observerManager; this.onPostMessage = this.handlePostMessage.bind(this); } MessageEventManager.prototype.handlePostMessage = function (event) { switch (event.origin) { case FRONT_URL: case CDN_URL: this.event(event.data, event.source, event.origin); break; } }; MessageEventManager.prototype.addPostMessageListener = function () { window.removeEventListener('message', this.onPostMessage); window.addEventListener('message', this.onPostMessage); }; MessageEventManager.prototype.event = function (data, source, origin) { this.observerManager.notify(MessageEventManager.EVENT, { data, source, origin }); }; RetryQueue.EVENT_CART = 'retry_queue_cart'; RetryQueue.EVENT_JOIN = 'retry_queue_join'; RetryQueue.EVENT_SEND_COLLECT = 'retry_queue_send_collect'; function RetryQueue({ userCacheManager, observerManager, hostingService, onsiteManager }) { /** @type {UserCacheManager} */ this.userCacheManager = userCacheManager; /** @type {ObserverManager} */ this.observerManager = observerManager; /** @type {HostingService} */ this.hostingService = hostingService; /** @type {OnsiteManager} */ this.onsiteManager = onsiteManager; this.eventTriggerManager = new EventTriggerManager({ retryQueue: this }); this.observer = new RetryQueueObserver({ observerManager, onsiteManager, retryQueue: this }); this.clientManager = new ClientManager(); this.process = []; this.pre_load = []; this.load_date = {}; this.timeoutId = null; this.retryCount = 0; this.maxRetries = 10; this.isScrolling = false; this.page_type = ''; this.eventId = 1; this.isPageExiting = false; this.enable = false; } // RetryQueue 프로토타입에 메서드 추가 RetryQueue.prototype.init = function (array, popstate = false) { this.observerManager.subscribe(this.observer.update.bind(this.observer)); if (!popstate) { this.eventTriggerManager.addEventListener(); } for (let i = 0; i < array.length; ++i) { this.enqueue(array[i]); } }; // 큐에 새 항목 추가 RetryQueue.prototype.enqueue = function (args) { STM_Util.storage.cookie.resetExpires(SNAPID); if (Array.isArray(args)) { args = args[0]; } if (this.enable === false) { this.preload(args); } else { this.sendQueue(args); } }; RetryQueue.prototype.preload = function (args) { if (args[0] === 'init') { this.sendEvent('SESSION_START', args); } else if (this.isInitCollect(args)) { if (args[0] === 'config') { if (!args[1]?.user_id) { this.sendPreloadQueue(); } } this.pre_load.push(args); } }; RetryQueue.prototype.sendPreloadQueue = async function () { await this.clientManager.init(); this.hostingService.setup({ hosting: STM_Util.hosting.getHosting() }); if (this.clientManager.clientId.prelive === 1) { await this.onsiteManager.preview(); } else if (this.clientManager.clientId.use === 1) { await this.onsiteManager.init(); } this.enable = true; for (let args of this.pre_load) { this.enqueue(args); } this.pre_load = []; }; RetryQueue.prototype.sendQueue = async function (args) { const utms = this.getUtmParams(); if (this.isInitCollect(args)) { if (args[1] === 'page_view') { this.sendEvent('PAGE', args); } else if (args[1] === 'order_complete') { args.is_member = stmParams.isMember ? 1 : 0; args.utms = utms; await this.sendEvent('ORDER', args); } else if (this.isAddUserId(args)) { this.clientManager.init(args[1]?.user_id); } else { this.sendEvent('INIT', args); } this.batchSend(); } else if (this.isCollectable(args)) { this.sendEvent('COLLECTABLE', args); } else if (this.isPageExit(args)) { this.isPageExiting = true; this.sendEvent('PAGE_EXIT', args); this.sendEvent('SESSION_END'); this.batchSend(); } }; RetryQueue.prototype.sendEvent = async function (type, args) { let payload = {}; switch (type) { case 'SESSION_START': payload = { 'stm.start': args[1].getTime(), event: 'stm.js', eventUniqueId: this.eventId++, }; break; case 'INIT': this.setEventArgs('INIT', args); payload = args; break; case 'PAGE': this.setEventArgs('INIT', args); this.page_type = args[2] && args[2].page_type; args.event_value = this.page_type; this.setPageViewDetail(args); this.setLastViewItem(args); payload = args; this.sendPageEvent(args); break; case 'BASKET': if (!(STM_Util.hosting.getHosting() === HOSTING_SHOPBY && !window.sb?.cart)) { payload = this.getBasket(); } break; case 'ORDER': payload = await this.getOrder(args); break; case 'JOIN': this.observerManager.notify(RetryQueue.EVENT_JOIN, { type: 'join' }); break; case 'COLLECTABLE': this.setEventArgs('COLLECTABLE', args); payload = args; break; case 'PAGE_EXIT': this.setEventArgs('PAGE_EXIT', args); payload = args; break; case 'SESSION_END': payload = { 'stm.end': Date.now(), event: 'stm.js', eventUniqueId: this.eventId++, }; break; } if (Object.keys(payload).length < 1) { return; } if (this.enable === true) { this.process.push(payload); } else { this.pre_load.push(payload); } }; RetryQueue.prototype.setEventArgs = function (type, args) { args.event_date = STM_Util.date.getCurrentDateTime(); args.eventUniqueId = this.eventId++; switch (type) { case 'INIT': args.url = location.href; args.ref_url = document.referrer; args.is_member = stmParams.isMember ? 1 : 0; args.utms = this.getUtmParams(); break; case 'PAGE_EXIT': args.event_name = args[1]; break; case 'COLLECTABLE': if (Array.isArray(args) === true) { args.event_name = args[1]; } break; } }; RetryQueue.prototype.getUtmParams = function () { let utms = STM_Util.url.getUTMParams(); // utm이 유효다면 저장 if (Object.keys(utms).length !== 0) { let utmStr = this.getSafeJsonParseData('string', utms); if (utmStr !== false) { localStorage.setItem(REF_UTM, utmStr); } } let storedUtms = localStorage.getItem(REF_UTM); if (storedUtms) { let parsedUtm = this.getSafeJsonParseData('parse', storedUtms); if (parsedUtm !== false) { utms = parsedUtm; } } return utms; }; RetryQueue.prototype.setPageViewDetail = function (args) { if (this.page_type === 'item_detail' || this.page_type === 'item_category') { args.event_name = 'page_view_detail'; } if (this.page_type === 'item_detail') { const itemData = STM_Util.hosting.getProductData(location.href); if (itemData && typeof itemData.productId === 'string') { args.event_value = itemData.productId; return; } } else if (this.page_type === 'item_category') { const categoryData = STM_Util.hosting.getCategoryData(location.href); if (categoryData && typeof categoryData.categoryNumber === 'string') { args.event_value = categoryData.categoryNumber; return; } } if (args[2] && args[2].item_id) { args.event_value = args[2].item_id; } }; RetryQueue.prototype.setLastViewItem = function (args) { if (this.page_type !== 'item_detail' || args[2]?.ignore_render) { return; } const itemId = args.event_value; if (itemId && typeof itemId === 'string') { STM_Util.storage.localStorage.set(LAST_VIEW_ITEM, itemId); } }; RetryQueue.prototype.sendPageEvent = function (args) { if (this.page_type === 'basket' && args[0] !== 'set') { this.sendEvent('BASKET'); } if (this.page_type === 'join_complete') { this.sendEvent('JOIN'); } }; RetryQueue.prototype.snaptag = function () { //내부에서 snaptag 부르면 dataQueue 삽입 //오버라이드된 push 는 retryQueue enqueue 호출됨 //snaptag 내부함수로 enqueue를해야 dataQueue 노출됨 그럴필요없으면 바로enqueue하면됨 dataQueue.push(arguments); }; RetryQueue.prototype.makeArguments = function () { // 배열을 arguments 객체로 변환하여 함수 호출 return arguments; }; RetryQueue.prototype.isUnsafeReferrerPolicy = function () { // 모든 메타 태그를 가져오기 let metaTags = document.getElementsByTagName('meta'); // 각 메타 태그를 확인 for (let i = 0; i < metaTags.length; i++) { let name = metaTags[i].getAttribute('name'); let content = metaTags[i].getAttribute('content'); if (name === 'referrer' && content === 'unsafe-url') { return true; } } return false; }; RetryQueue.prototype.getSafeJsonParseData = function (type, data) { try { let parsedData; let checktype; if (type === 'parse') { parsedData = JSON.parse(data); checktype = 'object'; } else { parsedData = JSON.stringify(data); checktype = 'string'; } if (parsedData && typeof parsedData === checktype) { return parsedData; } else { return false; } } catch (e) { return false; } }; //최초로 초기화후 수집하는코드인지 확인 RetryQueue.prototype.isInitCollect = function (args) { if (args[0] === 'event') { if (args[1] === 'page_view') { return true; } else if (args[1] === 'order_complete') { return true; } else if (args[1] === 'basket_list') { return true; } else if (args[1] === 'onsite_show') { return true; } else if (args[1] === 'onsite_click') { return true; } } else if (args && args[0] === 'config') { return true; } else if (args && args[0] === 'set') { return true; } else { return false; } }; RetryQueue.prototype.isCollectable = function (args) { if (args.event === 'stm.js') { return true; } if (args[0] === 'event') { return true; } return false; }; RetryQueue.prototype.isAddUserId = function (args) { if (args[0] === 'config') { if (args[1]?.user_id) { return true; } } return false; }; RetryQueue.prototype.isPageExit = function (args) { if (args[0] === 'event' && args[1] === 'page_exit' && this.isPageExiting === false) { if (args[2]) { args[2]['current_page'] = this.page_type; } return true; } else { return false; } }; RetryQueue.prototype.handleScroll = function () { this.isScrolling = true; // 이전 타임아웃이 있으면 클리어 if (this.timeoutId) { clearTimeout(this.timeoutId); } // 새로운 타임아웃 설정 this.timeoutId = setTimeout(() => { this.isScrolling = false; // 1초 동안 새로운 스크롤 이벤트가 없으면 스크롤이 멈춘 것으로 판단 let scrollPosition = window.scrollY || window.pageYOffset; /* this.snaptag('event', 'scrollDepth', { 'depth': scrollPosition }); */ this.batchSend(); this.timeoutId = null; // 타임아웃이 발생하면 timeoutId를 초기화 }, 1000); // 1초 후에 batchSend 호출 }; // 큐에 새 항목이 추가될 때 호출될 함수 RetryQueue.prototype.onQueueChange = function (args) { // 큐에 변화가 있을 때 sendBeacon을 호출하여 전송 시도 //this.trySendOrBatchSend(); // 스크롤 중이 아니라면 스크롤 이벤트 핸들러 등록 if (!this.isScrolling) { window.addEventListener('scroll', this.handleScroll.bind(this)); } //this.trySendBeacon(); }; RetryQueue.prototype.trySendOrBatchSend = function () { if (this.timeoutId) { clearTimeout(this.timeoutId); } // 새로운 타임아웃 설정 this.timeoutId = setTimeout(() => { this.batchSend(); this.timeoutId = null; }, 1000); // 1초 후에 batchSend 호출 }; // 일괄 전송 RetryQueue.prototype.batchSend = function () { for (let i = 0; i < this.process.length; i++) { let dataToSend = this.process[i]; this.retryBeacon(dataToSend); } // 실패한 이벤트들에 대해 재시도 this.retryFailedEvents(); this.resetRetryCount(); // 재시도 횟수 초기화 }; RetryQueue.prototype.getBasket = function () { let args = {}; let data = {}; let basketProductData = this.getBasketData(); if (basketProductData) { data.basketData = basketProductData; args = this.makeArguments('event', 'basket_list', data); args.is_member = stmParams.isMember ? 1 : 0; args.event_date = STM_Util.date.getCurrentDateTime(); args.event_name = args[1]; } return args; }; RetryQueue.prototype.getBasketData = function () { let basketProductData = []; let useOldPush = typeof snap_spm_banner_display === 'function'; if (useOldPush) { return false; } else { basketProductData = this.getCart(); } return basketProductData; }; RetryQueue.prototype.getCart = function () { const cart = []; const solution = STM_Util.hosting.getHosting(); switch (solution) { case HOSTING_CAFE24: window.aBasketProductData.map(x => { cart.push({ product_no: x.product_no, opt_str: x.option_str, item_code: x.item_code, basket_count: x.product_qty, product_sale_price: x.product_sale_price }); }); break; case HOSTING_MAKESHOP: if ($('form[name="forms"]')) { if ($('form[name="forms"]').length > 0) { $('form[name="forms"]').each(function (index) { let formData = $(this).serializeArray(); cart.push({ product_no: formData.find(x => x.name === 'branduid').value, opt_str: formData.find(x => x.name === 'snap_push_basket_option').value || '', item_code: formData.find(x => x.name === 'brandcode').value, basket_count: formData.find(x => x.name === 'amount').value, product_sale_price: $($('.tb-price')[index]).find('span').text().replaceAll(',', '') }); }); } } break; // 샵바이 case HOSTING_SHOPBY: if (window.sb?.cart) { window.sb?.cart?.deliveryGroups.map(items => items.orderProducts.map(item => item.orderProductOptions.map(option => { cart.push({ product_no: item.productNo, opt_str: option.optionNo || '', item_code: item.brandNo, basket_count: option.orderCnt, product_sale_price: option.price.salePrice }); }) ) ); } break; // 고도몰, 독립몰 case HOSTING_GODOMALL: default: if ($('input[name="cartSno[]"]')) { if ($('input[name="cartSno[]"]').length > 0) { $('input[name="cartSno[]"]').each(function () { cart.push({ product_no: $(this).data('goods-no'), opt_str: $(this).data('option-nm'), item_code: $(this).data('goods-key'), basket_count: $(this).data('default-goods-cnt'), product_sale_price: $(this).data('price') }); }); } } break; } return cart; }; RetryQueue.prototype.getOrder = async function (args) { args.event_name = args[1]; args.event_value = args[2]['orderNo']; args.event_date = STM_Util.date.getCurrentDateTime(); args.is_use_onsite = this.clientManager.clientId.use; //온사이트 사용여부 if (!Array.isArray(args[2].productList) || args[2].productList.length < 1) { args[2].productList = await this.getOrderProductList(); } return args; }; RetryQueue.prototype.getOrderProductList = async function () { let solution = STM_Util.hosting.getHosting(); const productList = []; switch (solution) { case HOSTING_CAFE24: { const hostingData = await STM_Util.hosting.cafe24.getOrder(); if (!hostingData) { return null; } for (const product of hostingData.items) { productList.push({ 'item_id': product.product_no, 'sale_price': product.product_price - Math.floor(product.additional_discount_price / product.quantity), 'price': product.product_price, 'order_cnt': product.quantity, }); } break; } case HOSTING_MAKESHOP: { const hostingData = snapPushOrderInstance; // order.js 의존 if (!hostingData) { return null; } for (const product of hostingData.productList) { productList.push({ 'item_id': product.productID, 'sale_price': product.sale_price, 'price': product.price, 'order_cnt': product.count, 'add_mileage': product.add_mileage, }); } break; } default: return null; } return productList; }; // 데이터 전송 시도 RetryQueue.prototype.sendData = async function (data) { let uuid = STM_Util.storage.cookie.get(SNAPA); let snapid = STM_Util.storage.cookie.get(SNAPID); let clientId = this.clientManager.getClientId(); let dl = STM_Util.storage.sdl.get(); let isCacheable = this.isCacheable(data); let nsu = STM_Util.storage.localStorage.get(NSU) || ''; let payload = { ...data, uuid, clientId, 'measurement_id': MEASUREMENT_ID, dl, 'su': STM_Util.storage.localStorage.get(SU), 'nsu': nsu, snapid, 'is_cacheable': isCacheable, }; if (this.supportsSendBeacon()) { // sendBeacon 지원 시 사용 if (navigator.sendBeacon(COLLECT_URL, JSON.stringify(payload))) { // 전송 성공 시 큐에서 항목 제거 this.removeProcessedData(data); return true; } } else { // sendBeacon을 지원하지 않는 경우 항상 XMLHttpRequest를 사용하여 전송 this.retryXhr(payload); } return false; // 전송 실패 }; RetryQueue.prototype.retryBeacon = async function (dataToSend) { let success = await this.sendData(dataToSend); if (!success) { return; } // 전송 성공 시 처리 this.removeProcessedData(dataToSend); if (dataToSend[0] === 'event') { this.observerManager.notify(RetryQueue.EVENT_SEND_COLLECT, dataToSend); } }; // 실패한 이벤트들에 대해 재시도 RetryQueue.prototype.retryFailedEvents = function (failedEvents) { for (let i = 0; i < this.process.length; i++) { let dataToSend = this.process[i]; if (this.supportsSendBeacon()) { this.retryBeacon(dataToSend); } else if (window.fetch) { return this.retryFetch(dataToSend); } else { return this.retryXhr(dataToSend); } } }; // sendBeacon을 이용하여 데이터 전송 시도 RetryQueue.prototype.trySendBeacon = function () { if (dataQueue.length > 0) { let dataToSend = dataQueue[0]; if (this.supportsSendBeacon()) { // sendBeacon 지원 시 사용 if (navigator.sendBeacon(FRONT_URL, JSON.stringify(dataToSend))) { // 전송 성공 시 큐에서 항목 제거 this.process.push(dataToSend); dataQueue.shift(); // 큐가 비었는지 확인 if (dataQueue.length === 0) { this.resetRetryCount(); // 전송이 성공하면 재시도 횟수 초기화 } } else { // 전송 실패 시 XMLHttpRequest를 이용하여 재시도 if (window.fetch) { return this.retryFetch(data); } else { return this.retryXhr(data); } } } else { // sendBeacon을 지원하지 않는 경우 항상 XMLHttpRequest를 사용하여 전송 if (window.fetch) { return this.retryFetch(data); } else { return this.retryXhr(data); } } } }; RetryQueue.prototype.retryXhr = function (data) { if (this.retryCount < this.maxRetries) { let xhr = new XMLHttpRequest(); xhr.open('POST', COLLECT_URL, true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onload = function () { if (xhr.status >= 200 && xhr.status < 300) { // 전송 성공 시 큐에서 항목 제거 this.removeProcessedData(data); // 나머지 코드 생략... } else { // 전송 실패 시 재시도 this.retryCount++; setTimeout(this.retryXhr.bind(this, data), 300000); // 5분(300,000 밀리초) 후 재시도 } }.bind(this); xhr.onload = function () { if (xhr.status >= 200 && xhr.status < 300) { // 전송 성공 시 큐에서 항목 제거 this.removeProcessedData(data); // 나머지 코드 생략... } else { // 전송 실패 시 재시도 this.retryCount++; setTimeout(this.retryXhr.bind(this, data), 300000); // 5분(300,000 밀리초) 후 재시도 } }.bind(this); // 네트워크 오류 발생 시 재시도 xhr.onerror = function () { this.retryCount++; setTimeout(this.retryXhr.bind(this, data), 300000); // 5분(300,000 밀리초) 후 재시도 }.bind(this); // 네트워크 오류 발생 시 재시도 xhr.onerror = function () { this.retryCount++; setTimeout(this.retryXhr.bind(this, data), 300000); // 5분(300,000 밀리초) 후 재시도 }.bind(this); // 데이터 전송 xhr.send(JSON.stringify(data)); } else { this.resetRetryCount(); // 최대 재시도 횟수 초과 시 재시도 횟수 초기화 } }; // XMLHttpRequest를 이용하여 데이터 전송 시도 RetryQueue.prototype.retryFetch = async function (data) { if (this.retryCount < this.maxRetries) { try { let response = await fetch(COLLECT_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), referrerPolicy: 'strict-origin-when-cross-origin' }); if (response.ok) { // 전송 성공 시 큐에서 항목 제거 this.removeProcessedData(data); return response; // 나머지 코드 생략... } else { // 전송 실패 시 재시도 this.retryCount++; setTimeout(() => this.retryFetch(data), 300000); // 5분(300,000 밀리초) 후 재시도 } } catch (e) { // 네트워크 오류 발생 시 재시도 this.retryCount++; setTimeout(() => this.retryFetch(data), 300000); // 5분(300,000 밀리초) 후 재시도 } } else { this.resetRetryCount(); // 최대 재시도 횟수 초과 시 재시도 횟수 초기화 } }; RetryQueue.prototype.removeProcessedData = function (dataToRemove) { // 성공한 데이터를 this.process 배열에서 제거 let index = this.process.indexOf(dataToRemove); if (index !== -1) { this.process.splice(index, 1); } }; // sendBeacon 지원 여부 확인 RetryQueue.prototype.supportsSendBeacon = function () { return typeof navigator.sendBeacon === 'function'; }; // 최대 재시도 횟수 초과 시 재시도 횟수 초기화 RetryQueue.prototype.resetRetryCount = function () { this.retryCount = 0; }; RetryQueue.prototype.isCacheable = function (data) { let eventType = data[1]; let targetCache = null; switch (eventType) { case 'page_view': targetCache = 'visit'; break; case 'onsite_show': return true; } return stmParams.cacheType.has(targetCache); }; RetryQueue.prototype.reinit = async function () { this.observerManager.subscribe(this.observer.update.bind(this.observer)); await this.sendPreloadQueue(); }; function EventTriggerManager({ retryQueue }) { /** @type {RetryQueue} */ this.retryQueue = retryQueue; this.eventNames = [ 'click', 'beforeunload', 'wheel', 'popstate', 'pushstate', 'hashchange', ]; } EventTriggerManager.prototype.addEventListener = function () { // window 객체의 이벤트 속성들 중에서 eventNames에 포함된 것들에 'on'을 다시 붙여서 가져오기 let windowEventNames = this.eventNames.map(function (eventName) { return 'on' + eventName; }).filter(function (prop) { return Object.prototype.hasOwnProperty.call(window, prop); }); // 각 이벤트 속성에 대해 이벤트 리스너 등록 windowEventNames.forEach((windowEventName) => { let eventName = windowEventName.substring(2); window.addEventListener(eventName, (event) => { this.eventCollect(eventName, event); }); }); }; EventTriggerManager.prototype.eventCollect = function (key, event) { // 여기서 정제 필요한 데이터 정제 // 이벤트 종류에 따라 다른 처리 switch (key) { case 'click': // 클릭 이벤트 처리 로직 let onclickName = event.srcElement.getAttribute('onClick'); let getId = event.srcElement.id; let aHref = event.srcElement.getAttribute('href'); event = this.extractEventData(event, key); if (window.CAFE24) { // 카페24 if (onclickName && onclickName.includes('product_submit(1')) { // 구매버튼 key = 'thumb_wish_purchase'; } else if (onclickName && onclickName.includes('add_wishlist')) { // 위시버튼 key = 'thumb_wish_add'; } else if (onclickName && onclickName.includes('product_submit(2')) { // 장바구니 버튼 key = 'thumb_cart_add'; } else if (getId && getId.includes('NPAY_BUY')) { // 네이버페이 구매 key = 'nPaybtn'; } else if (getId && getId.includes('NPAY_WISH')) { // 네이버페이 찜 key = 'nWishbtn'; } else if (event.classNames.length > 0 && event.classNames.includes('__checkout_btn_buy')) { // 카카오페이 구매 key = 'kakaoPaybtn'; } else if (event.classNames.length > 0 && event.classNames.includes('__checkout_btn_wish')) { // 카카오페이 찜 key = 'kakaoWishbtn'; } else if (event.classNames.length > 0 && event.classNames.includes('__checkout_btn_channel')) {// 카카오페이 채널추가 key = 'kakaoChannelbtn'; } else key = event.event; } else if (window.makeshopWishlist) { //메이크샵 if (aHref && (aHref.includes('send_multi') || aHref.includes('pre_send')) && aHref.includes('baro')) { // 바로구매 key = 'thumb_wish_purchase'; } else if (aHref && (aHref.includes('send_wish') || aHref.includes('pre_send_wish'))) { // 위시버튼 key = 'thumb_wish_add'; } else if (aHref && (aHref.includes('send_multi') || aHref.includes('pre_send')) && aHref.includes('baro') && aHref.includes('kakaopay_direct')) { // 카카오 구매 key = 'kakaoPaybtn'; } else if (aHref && (aHref.includes('send_multi') || aHref.includes('pre_send'))) { // 장바구니 key = 'thumb_cart_add'; } if (getId && getId.includes('NPAY_BUY')) { // 네이버페이 구매 key = 'nPaybtn'; } else if (getId && getId.includes('NPAY_WISH')) { // 네이버페이 찜 key = 'nWishbtn'; } else { key = event.event; } } else { key = event.event; } this.retryQueue.snaptag('event', key, event, Date.now()); break; case 'wheel': event = this.extractEventData(event, key); if (!this.retryQueue.isScrolling) { this.retryQueue.handleScroll(); } break; case 'load': // 로드 이벤트 처리 로직 this.retryQueue.snaptag('event', key, event, Date.now()); break; case 'beforeunload': // 페이지 이탈 처리 로직 this.retryQueue.snaptag('event', 'page_exit', { 'origin': window.location.href, 'referrer': document.referrer }); break; case 'visibilitychange': // 가시성 변경 이벤트 처리 로직 if (document.visibilityState === 'hidden') { // 페이지 이탈 처리 this.retryQueue.snaptag('event', 'page_exit', { 'origin': window.location.href, 'referrer': document.referrer }); } break; case 'popstate': this.retryQueue.snaptag('event', key, { 'origin': window.location.href, 'referrer': document.referrer }, Date.now()); break; case 'hashchange': this.retryQueue.snaptag('event', key, { 'hash': window.location.hash, 'origin': window.location.href, 'referrer': document.referrer }, Date.now()); break; default: // 공통 처리 (다른 이벤트에도 공통으로 적용되는 로직) this.retryQueue.snaptag('event', key, event, Date.now()); break; // 추가적인 이벤트에 대한 처리 추가 가능 } }; EventTriggerManager.prototype.extractEventData = function (event, key) { let eventData = {}; if (!event || typeof event !== 'object') { return eventData; } let targetElement = event.target; eventData = { event: event.type, // 이벤트 타입 tagName: targetElement.tagName, classNames: Array.from(targetElement.classList), key: key, href: '', itemData: '', event_type: '', }; let anchorElement = this.getAnchorElement(targetElement); if (anchorElement && key === 'click') { eventData.key = 'linkClick'; eventData.href = anchorElement.href; eventData.itemData = STM_Util.hosting.getProductData(anchorElement.href); // 'product_submit' 함수 호출 여부 확인 및 액션 타입 추출 eventData.event_type = this.getActionTypeFromElement(anchorElement); } return eventData; }; EventTriggerManager.prototype.getAnchorElement = function (clickedElement) { // 만약 클릭된 엘리먼트가 'a' 태그라면 그대로 반환 if (clickedElement.tagName.toLowerCase() === 'a') { return clickedElement; } // 부모에서 가장 가까운 'a' 태그를 찾아 반환, 없으면 null return clickedElement.closest('a'); }; EventTriggerManager.prototype.getActionTypeFromElement = function (element) { let eventType = ''; if (!element || typeof element !== 'object') { return eventType; } let hasProductSubmit = element && element.onclick && element.onclick.toString().includes('product_submit'); // 'product_submit' 함수 호출 여부 확인 if (hasProductSubmit) { // 'product_submit' 함수의 인자와 URL을 기반으로 '장바구니 담기'와 '바로구매'를 구분하여 반환 let parsedData = element.onclick.toString(); if (parsedData.includes('product_submit(2')) { eventType = 'addBasket'; } else if (parsedData.includes('product_submit(1')) { eventType = 'addBasket'; } } return eventType; }; function RetryQueueObserver({ observerManager, onsiteManager, retryQueue }) { /** @type {ObserverManager} */ this.observerManager = observerManager; /** @type {OnsiteManager} */ this.onsiteManager = onsiteManager; /** @type {RetryQueue} */ this.retryQueue = retryQueue; this.clickedCreative = []; } RetryQueueObserver.prototype.update = function (type, data) { switch (type) { case CampaignBase.EVENT_VIEW: this.handleCampaignView(data); break; case MessageEventManager.EVENT: switch (data.data.type) { case MessageEventManager.EVENT_PAGE_VIEW: this.retryQueue.snaptag('event', 'page_view', { page_type: 'item_detail', item_id: data.data.itemId, ignore_render: true, }); break; case MessageEventManager.EVENT_REFRESH_TOKEN: this.handleRefreshToken(data); break; } break; } }; RetryQueueObserver.prototype.handleRefreshToken = async function (data) { const token = await this.retryQueue.clientManager.refreshToken(data.data.token); if (!data.source) { return; } data.source.postMessage({ type: 'POST_MESSAGE_REFRESH_TOKEN_RESPONSE', token }, data.origin); }; RetryQueueObserver.prototype.handleCampaignView = function (data) { const campaign = this.onsiteManager.getCampaign(data.campaignId); if (!campaign || STM_Util.url.isPreview()) { return; } if (campaign.creativeType === 'product_search') { const snapid = STM_Util.storage.cookie.get(SNAPID); if (snapid) { const session = STM_Util.storage.localStorage.get(CAMPAIGN_SESSION_SHOW) || {}; if (session.snapid !== snapid) { session.snapid = snapid; session.campaigns = []; } if (session.campaigns.includes(campaign.id)) { return; } session.campaigns.push(campaign.id); STM_Util.storage.localStorage.set(CAMPAIGN_SESSION_SHOW, session); } } this.retryQueue.snaptag('event', 'onsite_show', { campaignId: data.campaignId, creativeId: data.creativeId, contentIds: data.contentIds, creativeType: campaign.getCreativeType(), }); }; function ClientManager() { this.snapa = STM_Util.storage.cookie.get(SNAPA); this.iu = ''; this.nsu = ''; this.su = STM_Util.storage.localStorage.get(SU); this.snapuid = STM_Util.storage.localStorage.get(SNAPUID); this.clientId = {}; this.refreshPromise = null; } ClientManager.prototype.init = async function (userid = null) { await this.setClientId(userid); this.setIsMember(); this.su = this.clientId.su; STM_Util.storage.localStorage.set(SU, this.su); if (stmParams.isMember) { this.snapuid = this.clientId.snapuid; STM_Util.storage.localStorage.set(SNAPUID, this.snapuid); } }; ClientManager.prototype.setIsMember = function () { let hasUserId = Boolean(this.iu); let hasSnapuid = Boolean(this.snapuid); stmParams.isMember = hasUserId || hasSnapuid; }; ClientManager.prototype.setClientId = async function (userid = null) { let hasSdl = STM_Util.storage.sdl.get(); this.clientId.sdl = hasSdl; this.iu = userid || STM_Util.hosting.getUserId(); let use = sessionStorage.getItem('snap'); this.clientId = await this.fetchClientId(); if (this.clientId.hosting) { STM_Util.storage.localStorage.set(HOSTING, this.clientId.hosting); } if (this.clientId.nsu) { STM_Util.storage.localStorage.set(NSU, this.clientId.nsu); } if (this.clientId.idu) { STM_Util.customItemUrl = this.clientId.idu; } if (this.clientId.accessTokenKey) { STM_Util.shopbyAuth.tokenKey = this.clientId.accessTokenKey; } if (this.clientId.accessTokenJsonKey) { STM_Util.shopbyAuth.tokenJsonKey = this.clientId.accessTokenJsonKey; } if (this.clientId.cid) { STM_Util.shopbyAuth.clientId = this.clientId.cid; } STM_Util.storage.sdl.set(this.clientId.sdl); }; ClientManager.prototype.fetchClientId = async function (userid = null) { try { this.iu = STM_Util.hosting.getUserId(); const currentHost = window.location.host; const currentHref = window.location.href; const uuid = STM_Util.storage.cookie.get(SNAPA); const solution = STM_Util.hosting.getHosting(); let destUrl = GENERATE_CLIENT; let agent = navigator.userAgentData; let nsu = STM_Util.storage.localStorage.get(NSU); if (!nsu) { nsu = ''; } if (solution == 'makeshop') { destUrl = GENERATE_CLIENT_RETRY; } const response = await fetch(destUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ 'dl': DL, 'iu': this.iu, 'nsu': nsu, 'snapuid': this.snapuid, 'uuid': uuid, 'currentHost': currentHost, 'currentHref': currentHref, 'agent': agent }), credentials: 'include', referrerPolicy: 'strict-origin-when-cross-origin', }); if (!response.ok) { throw new Error(`Failed to fetch iu. Status: ${response.status}`); } const data = await response.json(); return data; } catch (e) { return {}; } }; ClientManager.prototype.retryClientId = async function () { try { const currentHost = window.location.host; const currentHref = window.location.href; const destUrl = GENERATE_CLIENT_RETRY; const response = await fetch(destUrl, { method: 'POST', headers: { 'Content-Type': 'text/plain', }, body: JSON.stringify({ 'dl': DL, 'iu': this.iu, 'nsu': nsu, 'snapuid': this.snapuid, 'currentHost': currentHost, 'currentHref': currentHref }), credentials: 'include', referrerPolicy: 'strict-origin-when-cross-origin' }); const data = await response.json(); return data; } catch (error) { return {}; } }; ClientManager.prototype.getClientId = function () { return this.snapuid || this.snapa; }; ClientManager.prototype.refreshToken = async function (requesterToken = null) { const stored = STM_Util.storage.sdl.get(); if (stored && requesterToken && stored !== requesterToken) { return stored; } if (!stored) { return null; } if (this.refreshPromise) { return this.refreshPromise; } this.refreshPromise = (async () => { try { const response = await fetch(REFRESH_TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ dl: stored }), }); if (!response.ok) { return null; } const result = await response.json(); if (!result.sdl) { return null; } STM_Util.storage.sdl.set(result.sdl); return result.sdl; } catch (error) { return null; } finally { this.refreshPromise = null; } })(); return this.refreshPromise; }; OnsiteManager.EVENT_RESPONSE = 'onsite_manager_response'; OnsiteManager.REQUEST_TYPE = {}; OnsiteManager.REQUEST_TYPE.OPTION = 'option'; OnsiteManager.REQUEST_TYPE.INTERVAL = {}; OnsiteManager.REQUEST_TYPE.INTERVAL.CAMPAIGN = 'campaignInterval'; OnsiteManager.REQUEST_TYPE.CACHE_TYPE = 'cacheType'; OnsiteManager.REQUEST_TYPE.SOCIAL = 'social'; OnsiteManager.REQUEST_TYPE.COUPON = 'coupon'; OnsiteManager.REQUEST_TYPE.ITEM = {}; OnsiteManager.REQUEST_TYPE.ITEM.CATEGORY = 'itemCategory'; OnsiteManager.REQUEST_TYPE.ITEM.NAME = 'itemName'; OnsiteManager.REQUEST_TYPE.ITEM.TAG = 'itemTag'; function OnsiteManager({ userCacheManager, observerManager, hostingService }) { /** @type {UserCacheManager} */ this.userCacheManager = userCacheManager; /** @type {ObserverManager} */ this.observerManager = observerManager; /** @type {HostingService} */ this.hostingService = hostingService; this.campaignManager = new CampaignManager({ userCacheManager, observerManager, onsiteManager: this }); this.observer = new OnsiteObserver({ userCacheManager: this.userCacheManager, campaignManager: this.campaignManager, hostingService: this.hostingService, }); this.requestData = { [OnsiteManager.REQUEST_TYPE.CACHE_TYPE]: new Set(), }; } OnsiteManager.prototype.preview = async function () { this.observerManager.subscribe(this.observer.update.bind(this.observer)); this.campaignManager.preview(); }; OnsiteManager.prototype.init = async function () { try { await Promise.all([ this.userCacheManager.init(), this.campaignManager.init(), ]); this.observerManager.subscribe(this.observer.update.bind(this.observer)); const requestPayload = this.buildRequestPayload(); if (Object.keys(requestPayload).length === 0) { return; } const onsiteConfig = await this.fetchOnsiteConfig(requestPayload); if (Object.keys(onsiteConfig).length < 1) { return; } if (onsiteConfig.user_cache) { this.userCacheManager.setUserCache(onsiteConfig.user_cache); } if (onsiteConfig.click_cache) { this.userCacheManager.setClickCache(onsiteConfig.click_cache); } if (onsiteConfig.itemCategory) { stmParams.itemCategory = onsiteConfig.itemCategory; } if (onsiteConfig.itemName) { stmParams.itemName = onsiteConfig.itemName; } if (onsiteConfig.creative) { this.creativeEvaluate = onsiteConfig.creative; } await this.updateCart(); this.observerManager.notify(OnsiteManager.EVENT_RESPONSE, onsiteConfig); } catch (error) { console.error(error); } }; OnsiteManager.prototype.resetIframe = function () { document.querySelectorAll('[id]').forEach(el => { if (/\bsnap_onsite_/i.test(el.id)) el.remove(); }); this.observerManager.observers = []; this.observer.response = false; }; OnsiteManager.prototype.registerRequestData = function (type, value) { switch (type) { case OnsiteManager.REQUEST_TYPE.OPTION: case OnsiteManager.REQUEST_TYPE.INTERVAL.CAMPAIGN: if (!this.requestData[type]) { this.requestData[type] = []; } this.requestData[type].push(value); break; case OnsiteManager.REQUEST_TYPE.CACHE_TYPE: stmParams.cacheType.add(value); case OnsiteManager.REQUEST_TYPE.COUPON: if (!this.requestData[type]) { this.requestData[type] = new Set(); } this.requestData[type].add(value); break; case OnsiteManager.REQUEST_TYPE.ITEM.CATEGORY: case OnsiteManager.REQUEST_TYPE.ITEM.NAME: this.requestData[type] = value; break; case OnsiteManager.REQUEST_TYPE.ITEM.TAG: const { itemSeq, tags } = value; if (!this.requestData.itemTag) { this.requestData.itemTag = {}; } if (!this.requestData.itemTag[itemSeq]) { this.requestData.itemTag[itemSeq] = new Set(); } tags.forEach((tag) => { this.requestData.itemTag[itemSeq].add(tag); }); break; case OnsiteManager.REQUEST_TYPE.SOCIAL: if (!this.requestData.social) { this.requestData.social = {}; } const campaignSeq = value.campaignSeq; const date = value.date; this.requestData.social[campaignSeq] = date; break; } }; OnsiteManager.prototype.buildRequestPayload = function () { const payload = structuredClone(this.requestData); if (payload.social && Object.keys(payload.social).length > 0) { const productData = STM_Util.hosting.getProductData(location.href); payload.itemId = productData.productId; } const pageType = STM_Util.getPageType(); const isNeedItemCategory = pageType === 'order_complete'; if (isNeedItemCategory) { payload.itemCategory = true; } if (payload.itemTag && Object.keys(payload.itemTag).length > 0) { Object.keys(payload.itemTag).forEach((itemSeq) => { payload.itemTag[itemSeq] = Array.from(payload.itemTag[itemSeq]); }); } Object.keys(payload).forEach((key) => { if (payload[key] instanceof Set) { payload[key] = Array.from(payload[key]); } }); if (this.campaignManager.campaigns) { payload.creative = Object.entries(this.campaignManager.campaigns).map(x => x[1].creative.id); } return payload; }; OnsiteManager.prototype.fetchOnsiteConfig = async function (requestPayload) { try { let token = STM_Util.storage.sdl.get(); if (!token) { return; } let response = await fetch(ONSITE_GET, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(requestPayload), referrerPolicy: 'strict-origin-when-cross-origin', credentials: 'include', }); let data = await response.json(); if (data && data.error) { throw new Error(`Failed to fetch onsite config. error: ${data.error}`); } return data; } catch (e) { return {}; } }; OnsiteManager.prototype.updateCart = async function () { const hosting = STM_Util.hosting.getHosting(); const curPage = STM_Util.getPageType(); // 고도몰 주문 페이지에서 장바구니를 조회 시 결제 불가 이슈 발생 // - 고도몰: 이슈 확인된 케이스 // - 메이크샵: 고도몰과 동일 로직이라 잠재 위험이 있어 함께 예외 처리 // 따라서 두 호스팅의 주문 페이지에서는 장바구니를 조회하지 않는다 if ((hosting === HOSTING_MAKESHOP || hosting === HOSTING_GODOMALL) && curPage === 'order') { return; } const cart = await this.hostingService.getCart(); await this.userCacheManager.updateEvent({ type: 'cart', data: cart }); }; OnsiteManager.prototype.getCampaign = function (campaignId) { const campaign = this.campaignManager.getCampaign(campaignId); return campaign; }; UserCacheManager.EVENT_UPDATE = 'user_cache_update'; function UserCacheManager({ db, observerManager }) { /** @type {DBManager} */ this.db = db; /** @type {ObserverManager} */ this.observerManager = observerManager; this.dataStorage = new DataStorage({ db }); this.session = new Session({ db }); } UserCacheManager.prototype.init = async function () { Promise.all([ this.dataStorage.init(), this.session.init(), ]); if (stmParams.isMember) { this.login(); } }; UserCacheManager.prototype.login = async function () { const snapa = STM_Util.storage.cookie.get(SNAPA); const nonMemberInfo = await this.db.select(DB_DATA_STORAGE, snapa); if (!nonMemberInfo) { return; } const memberInfo = { su: STM_Util.storage.localStorage.get(SNAPUID), }; if (nonMemberInfo.onsite_click && Object.keys(nonMemberInfo.onsite_click)) { memberInfo.onsite_click = nonMemberInfo.onsite_click; } await this.dataStorage.set(memberInfo); await this.db.delete(DB_DATA_STORAGE, snapa); }; UserCacheManager.prototype.setUserCache = function (userCache) { const stored = this.dataStorage.get(); Object.assign(stored, userCache); this.dataStorage.set(stored); }; UserCacheManager.prototype.setClickCache = function (clickCache) { const stored = this.dataStorage.get(); Object.assign(stored, { onsite_click: clickCache }); this.dataStorage.set(stored); }; UserCacheManager.prototype.getDataStorage = function () { return this.dataStorage; }; UserCacheManager.prototype.getSession = function () { return this.session; }; UserCacheManager.prototype.updateEvent = async function (eventData) { if (eventData === null || typeof eventData === 'undefined') { return; } let releaseLock = await this.db.requestLock(); let type = eventData[1] || eventData.type; let data = eventData[2] || eventData.data; if (eventData.event_date) { data.event_date = eventData.event_date; } let updateData = await this.getUpdateData(type, data); if (Object.keys(updateData).length === 0) { releaseLock(); return; } await Promise.all([ this.dataStorage.update(updateData), this.session.update(updateData), ]); this.observerManager.notify(UserCacheManager.EVENT_UPDATE, eventData); releaseLock(); }; UserCacheManager.prototype.getUpdateData = async function (type, data) { let updateData = {}; switch (type) { case 'page_view': updateData.type = 'visit'; updateData.pageType = data.page_type; updateData.page = this.extractVisitData(data); break; case 'order_complete': updateData.type = 'order'; updateData.orderId = await STM_Util.security.sha256(data.orderNo); updateData.mainOrder = this.extractOrderData(data); if (updateData.mainOrder.length < 1) { updateData = {}; } break; case 'cart': updateData.type = 'cart'; updateData.cart = this.extractCartData(data); break; case 'onsite_show': updateData.type = 'onsite_show'; updateData.campaign_id = data.campaignId; updateData.creativeType = data.creativeType; updateData.event_date = data.event_date; break; case 'click': updateData.type = 'onsite_click'; updateData.campaign_id = data.campaignId; updateData.creative_id = data.creativeId; updateData.content_id = data.contentId; updateData.event_date = data.event_date; break; case 'coupon': break; case 'join': updateData.type = 'client'; updateData.data = type; break; } return updateData; }; UserCacheManager.prototype.extractVisitData = function (data) { const pageType = data.page_type; let page = 'undefined'; switch (pageType) { case 'item_detail': { const itemData = STM_Util.hosting.getProductData(location.href); page = itemData.productId; break; } case 'item_category': { const itemData = STM_Util.hosting.getCategoryData(location.href); page = itemData.categoryNumber; break; } default: page = PAGE_MAP[data.page_type] ?? page; break; } if (data.item_id) { page = data.item_id; } return page; }; UserCacheManager.prototype.extractOrderData = function (data) { if (!data || !Array.isArray(data.productList)) { return []; } const hosting = STM_Util.hosting.getHosting(); const orderDate = STM_Util.hosting.getOrderDate(data.orderNo); const totalDiscount = Number(String(data.totalDiscount).includes(".") ? data.totalDiscount.split('.')[0] : data.totalDiscount); const useMileage = Number(data.useMileage); const payedPrice = Number(data.payPrice); const totalPrice = payedPrice + totalDiscount + useMileage; const mainOrder = { total_price: totalPrice, payed_price: payedPrice, orders: [], }; for (const product of data.productList) { let salePrice = Number(product.sale_price); if (hosting === HOSTING_GODOMALL) { const price = Number(product.price); salePrice = price - salePrice; } const order = { date: orderDate, status: 'order', item_id: product.item_id, cate_id: stmParams.itemCategory[product.item_id] ?? '', sale_price: salePrice, order_cnt: Number(product.order_cnt), }; mainOrder.orders.push(order); } return mainOrder; }; UserCacheManager.prototype.extractCartData = function (data) { if (!data || !Array.isArray(data)) { return {}; } const cart = {}; for (const { item_id, count, sale_price } of data) { if (!cart[item_id]) { cart[item_id] = { count: 0, total_price: 0 }; } cart[item_id].count += Number(count); cart[item_id].total_price += Number(sale_price) * Number(count); } return cart; }; function UserCache(dependency) { STM_BaseEntity.call(this, dependency); } UserCache.prototype = Object.create(STM_BaseEntity.prototype); UserCache.prototype.constructor = UserCache; UserCache.prototype.update = function () { }; UserCache.prototype.setVisitData = function (curData, updateData) { const pageType = updateData.pageType; const page = updateData.page; let visitCount = 0; switch (pageType) { case 'item_detail': { if (curData.item === null || typeof curData.item === 'undefined') { curData.item = {}; } visitCount = curData.item[page] ?? 0; curData.item[page] = visitCount + 1; break; } case 'item_category': { if (curData.category === null || typeof curData.category === 'undefined') { curData.category = {}; } visitCount = curData.category[page] ?? 0; curData.category[page] = visitCount + 1; break; } default: visitCount = curData[page] ?? 0; curData[page] = visitCount + 1; break; } }; UserCache.prototype.setOrderData = function (curData, updateData) { let orderId = updateData.orderId; let order = updateData.mainOrder; curData[orderId] = order; }; function DataStorage(dependency) { UserCache.call(this, dependency); this.dbName = DB_DATA_STORAGE; } DataStorage.prototype = Object.create(UserCache.prototype); DataStorage.prototype.constructor = DataStorage; DataStorage.prototype.update = async function (data) { let result = await this.db.select(this.dbName); this.data = result || {}; let type = data.type; if (type === null || typeof type === 'undefined') { return; } switch (type) { case 'visit': if (this.data.visit === null || typeof this.data.visit === 'undefined') { this.data.visit = {}; } if (this.data.visit[STM_Util.date.getDate(0)] === null || typeof this.data.visit[STM_Util.date.getDate(0)] === 'undefined') { this.data.visit[STM_Util.date.getDate(0)] = {}; } this.setVisitData(this.data.visit[STM_Util.date.getDate(0)], data); break; case 'order': if (this.data.order === null || typeof this.data.order === 'undefined') { this.data.order = {}; } this.setOrderData(this.data.order, data); break; case 'cart': this.setCartData(this.data.cart, data); break; case 'onsite_show': this.setOnsiteInterval(data); this.setCampaignInterval('view', data); break; case 'onsite_click': this.setCampaignInterval('click', data); this.setLastClick(data); break; default: return; } await this.db.update(this.dbName, this.data); }; DataStorage.prototype.setCartData = function (curData, updateData) { if (this.data.cart === null || typeof this.data.cart === 'undefined') { this.data.cart = {}; } this.data.cart[STM_Util.date.getDate(0)] = updateData.cart; }; DataStorage.prototype.setOnsiteInterval = function (data) { const campaignId = data.campaign_id; const creativeType = data.creativeType; if (this.data.onsite_interval === null || typeof this.data.onsite_interval === 'undefined') { this.data.onsite_interval = {}; } this.data.onsite_interval[creativeType] = {}; this.data.onsite_interval[creativeType].seq = campaignId; this.data.onsite_interval[creativeType].last = data.event_date; }; DataStorage.prototype.setCampaignInterval = function (type, data) { const campaignId = data.campaign_id; if (this.data.campaign_interval === null || typeof this.data.campaign_interval === 'undefined') { this.data.campaign_interval = {}; } if (this.data.campaign_interval[campaignId] === null || typeof this.data.campaign_interval[campaignId] === 'undefined') { this.data.campaign_interval[campaignId] = { view: null, click: null, count: 0, }; } if (type === 'view') { this.data.campaign_interval[campaignId].count++; } this.data.campaign_interval[campaignId][type] = data.event_date ?? STM_Util.date.getCurrentDateTime(); }; DataStorage.prototype.setLastClick = function (data) { if (this.data.onsite_click === null || typeof this.data.onsite_click === 'undefined') { this.data.onsite_click = {}; } this.data.onsite_click = { campaign_id: data.campaign_id, creative_id: data.creative_id, content_id: data.content_id, }; }; DataStorage.prototype.get = function (type) { if (type === null || typeof type === 'undefined') { return this.data; } return this.data[type] ?? {}; }; function Session(dependency) { UserCache.call(this, dependency); this.dbName = DB_SESSION; } Session.prototype = Object.create(UserCache.prototype); Session.prototype.constructor = Session; Session.prototype.update = async function (data) { this.data = await this.db.select(this.dbName) || {}; let type = data.type; if (!this.data[type]) { this.data[type] = {}; } switch (type) { case 'visit': this.setVisitData(this.data[type], data); break; case 'order': this.setOrderData(this.data[type], data); break; case 'cart': this.setCartData(this.data[type], data); break; case 'client': if (data.data === 'join') { this.data[type].join = true; } break; case 'onsite_show': this.setCampaignView(this.data[type], data); break; case 'onsite_click': this.data.onsite_click = { campaign_id: data.campaign_id, creative_id: data.creative_id, content_id: data.content_id, }; break; default: delete this.data[type]; break; } await this.db.update(this.dbName, this.data); }; Session.prototype.get = function (type) { if (type === null || typeof type === 'undefined') { return this.data; } return this.data[type] ?? {}; }; Session.prototype.setCartData = function (curData, updateData) { const lastestDate = Object.keys(curData).sort((a, b) => { return b - a; })[0]; const lastestCart = curData[lastestDate]; const updateCart = updateData.cart; if (JSON.stringify(lastestCart) === JSON.stringify(updateCart)) { return; } curData[Date.now()] = updateCart; }; Session.prototype.setCampaignView = function (curData, updateData) { const campaignId = updateData.campaign_id; curData[campaignId] = (curData[campaignId] ?? 0) + 1; }; function CampaignManager({ userCacheManager, observerManager, onsiteManager }) { /** @type {UserCacheManager} */ this.userCacheManager = userCacheManager; /** @type {ObserverManager} */ this.observerManager = observerManager; /** @type {OnsiteManager} */ this.onsiteManager = onsiteManager; this.campaigns = {}; } CampaignManager.prototype.init = async function () { const setting = await this.fetchSetting(); const campaignsConfig = setting.campaigns; this.campaigns = {}; this.initCampaigns(campaignsConfig); this.preview(); // 캠페인 상세 > 미리보기 }; CampaignManager.prototype.preview = function () { const hash = window.location.hash.startsWith('#'); if (hash === true) { const searchParams = new URLSearchParams(window.location.hash); // 안전하게 파라미터 추출 (없는 경우 null 반환) const campaignSeq = searchParams.get('campaignSeq'); const creativeSeq = searchParams.get('creativeSeq'); const token = STM_Util.storage.sdl.get(); // 파라미터가 없으면 요청하지 않음 if (!campaignSeq || !creativeSeq || !token) { return; // 필수 파라미터가 없으면 요청을 보내지 않음 } // 요청 보낼 데이터 (추출한 파라미터만 포함) const bodyData = { campaignSeq: campaignSeq, creativeSeq: creativeSeq }; // POST 요청 보내기 (쿠키 동봉) fetch(PREVIEW_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, credentials: 'include', body: JSON.stringify(bodyData), }) .then(response => response.json()) .then(data => { this.renderPreview(data); }) .catch(error => { }); } }; CampaignManager.prototype.fetchSetting = async function () { try { let response = await fetch(SETTING_CDN_URL); if (!response.ok) { throw new Error('캠페인 정보를 가져오지 못했습니다.'); } let data = await response.json(); data = STM_Util.convert.parseBoolean(data); // for qa: start const filtered = { campaigns: {} }; const campaigns = data.campaigns; const queryParams = STM_Util.url.getQueryParams(); let target = queryParams['qa_campaign_id'] ?? ''; target = target.split(',').filter(Boolean); if (Array.isArray(target) && target.length > 0) { for (const id of Object.keys(campaigns)) { if (target.includes(id)) { filtered.campaigns[id] = campaigns[id]; } } return filtered; } // for qa: end return data; } catch (e) { return {}; } }; CampaignManager.prototype.initCampaigns = function (config) { if (config === null || typeof config === 'undefined') { return; } for (const campaignId of Object.keys(config)) { const campaignConfig = config[campaignId]; const campaign = new CampaignBase({ userCacheManager: this.userCacheManager, observerManager: this.observerManager, onsiteManager: this.onsiteManager, }); campaign.init(campaignId, campaignConfig); if (!campaign.evaluate('precondition')) { continue; } this.campaigns[campaignId] = campaign; } }; CampaignManager.prototype.getCampaigns = function () { return this.campaigns; }; /** * @param {Number} campaignSeq * @returns {CampaignBase} */ CampaignManager.prototype.getCampaign = function (campaignSeq) { return this.campaigns[campaignSeq]; }; CampaignManager.prototype.renderPreview = async function (previewData) { if (!previewData || previewData.campaignSeq === null || typeof previewData.campaignSeq === 'undefined' || previewData.creativeSeq === null || typeof previewData.creativeSeq === 'undefined') { return; } let campaignId = previewData.campaignSeq; let creativeId = previewData.creativeSeq; let campaign = new CampaignBase({ userCacheManager: this.userCacheManager, observerManager: this.observerManager, onsiteManager: this.onsiteManager, }); this.campaigns[campaignId] = campaign; campaign.init(campaignId, previewData.campaignData); campaign.render(); }; CampaignBase.EVENT_VIEW = 'campaign_base_view'; function CampaignBase({ userCacheManager, observerManager, onsiteManager }) { /** @type {UserCacheManager} */ this.userCacheManager = userCacheManager; /** @type {ObserverManager} */ this.observerManager = observerManager; /** @type {OnsiteManager} */ this.onsiteManager = onsiteManager; this.creative = new CreativeBase({ userCacheManager, observerManager, onsiteManager }); this.evaluator = new ConditionBase(); this.id = null; this.creativeType = null; this.priority = null; this.liveDate = new Date(); this.isIgnorePriority = 0; this.abtestSeq = null; } CampaignBase.prototype.init = function (id, config) { this.id = id; this.creativeType = config.creative_type; this.priority = config.priority; this.liveDate = config.live_date ? new Date(config.live_date) : new Date(); this.isIgnorePriority = Number(config.is_use_ignore_priority); this.abtestSeq = config.abtest_seq; this.initCreative(config.creative, config.abtest_seq); this.initEvaluator(config.condition); }; CampaignBase.prototype.initCreative = function (creativeConfig, abtestSeq) { if (creativeConfig === null || typeof creativeConfig === 'undefined' || typeof creativeConfig !== 'object') { return; } let creativeId = null; if (abtestSeq) { creativeId = this.getAbtestCreativeId(abtestSeq, creativeConfig); } else { creativeId = Object.keys(creativeConfig)[0]; } if (creativeId) { this.creative.init(creativeId, this.id, this.creativeType, creativeConfig[creativeId]); } }; CampaignBase.prototype.getAbtestCreativeId = function (abtestSeq, creatives) { const creativeIds = Object.keys(creatives).sort((a, b) => { return Number(a) - Number(b); }); const bucket = this.getUserBucket(abtestSeq, creativeIds.length); const selected = creativeIds[bucket]; return selected; }; CampaignBase.prototype.getUserBucket = function (seq, num) { const key = seq + ':' + String(STM_Util.storage.localStorage.get(SU)); const MAX = 4294967296; const limit = MAX - (MAX % num); const h = STM_Util.security.fnv1a32(key); let salt = 0; while (h >= limit) { salt++; h = STM_Util.security.fnv1a32(key + ':' + salt); } return h % num; }; CampaignBase.prototype.initEvaluator = function (condition) { const dependency = { userCacheManager: this.userCacheManager, observerManager: this.observerManager, onsiteManager: this.onsiteManager, creative: this.creative, }; switch (this.creativeType) { case 'social': this.evaluator = new SocialEvaluator(dependency); this.evaluator.init(this.id, condition); break; case 'product_search': this.evaluator = new ProductSearchEvaluator(dependency); break; default: this.evaluator = new CampaignEvaluator(dependency); this.evaluator.init(this.id, this.creativeType, condition); break; } }; CampaignBase.prototype.evaluate = function (type) { return this.evaluator.evaluate(type); }; CampaignBase.prototype.getCreativeType = function () { return this.creativeType; }; CampaignBase.prototype.getCreative = function () { return this.creative; }; CampaignBase.prototype.getIsIgnorePriority = function () { return this.isIgnorePriority; }; CampaignBase.prototype.getPriorityValue = function () { return Number(this.priority) || 0; }; CampaignBase.prototype.getLiveDate = function () { return this.liveDate; }; CampaignBase.prototype.render = function () { this.creative.render(); }; CampaignBase.prototype.display = function (config) { this.initIntersectionObserver(config); if (this.creative.getHasInteraction()) { this.creative.handleInteraction(config); } else { this.creative.display(config); } }; CampaignBase.prototype.setSize = function (width, height) { this.creative.setSize(width, height); }; CampaignBase.prototype.close = function () { this.creative.close(); }; CampaignBase.prototype.trigger = function () { this.creative.trigger(); }; CampaignBase.prototype.todayNoShow = function () { STM_Util.storage.cookie.create(CAMPAIGN_TODAY_NO_SHOW + this.id, true); this.creative.close(); }; CampaignBase.prototype.initIntersectionObserver = function (config) { const iframe = this.creative.getIframe(); if (!iframe) { return; } if (this.creativeType === 'product_search' && config.iframeIndex === 0) { return; } const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) { return; } this.observerManager.notify(CampaignBase.EVENT_VIEW, config); observer.unobserve(iframe); // 최초 1회만 감지 }); }); observer.observe(iframe); }; function EvaluatorBase() { } EvaluatorBase.prototype.evaluate = function () { return true; }; function CampaignEvaluator({ userCacheManager, observerManager, onsiteManager, creative }) { /** @type {CreativeBase} */ this.creative = creative; this.intervalEvaluator = new IntervalCondition({ userCacheManager, observerManager, onsiteManager, creative }); this.restrictEvaluator = new RestrictEvaluator({ userCacheManager, observerManager, onsiteManager }); this.campaignId = null; this.creativeType = null; this.user = null; } CampaignEvaluator.prototype.init = function (campaignId, creativeType, condition) { this.campaignId = campaignId; this.creativeType = creativeType; this.user = condition.user; this.intervalEvaluator.init(this.campaignId, this.creativeType, condition.setting); this.restrictEvaluator.init(condition.restrict); }; CampaignEvaluator.prototype.evaluate = function (type) { if (type === 'precondition') { return this.evaluatePrecondition(); } else if (type === 'condition') { return this.evaluateCondition(); } return false; }; CampaignEvaluator.prototype.evaluatePrecondition = function () { if (!this.evaluateTodayNoShow()) { return false; } if (!this.evaluateUserCondition()) { return false; } if (!this.creative.evaluate('precondition')) { return false; } return true; }; CampaignEvaluator.prototype.evaluateTodayNoShow = function () { let todayNoShow = STM_Util.storage.cookie.get(CAMPAIGN_TODAY_NO_SHOW + this.campaignId); return !todayNoShow; }; CampaignEvaluator.prototype.evaluateUserCondition = function () { switch (this.user) { case 'none': case 'all': return true; case 'non_member': return !stmParams.isMember; case 'member': return stmParams.isMember; default: return false; } }; CampaignEvaluator.prototype.evaluateCondition = function () { if (!this.creative.evaluate('condition')) { return false; } if (!this.intervalEvaluator.evaluate()) { return false; } if (this.user === 'none') { return true; } if (!this.restrictEvaluator.evaluate()) { return false; } return true; }; function SocialEvaluator({ userCacheManager, observerManager, onsiteManager, creative }) { this.intervalEvaluator = new SocialIntervalCondition({ userCacheManager }); this.restrictEvaluator = new SocialCondition({ userCacheManager, observerManager, onsiteManager, creative }); this.campaignId = null; } SocialEvaluator.prototype.init = function (campaignId, condition) { this.campaignId = campaignId; this.intervalEvaluator.init(this.campaignId, condition.setting); this.restrictEvaluator.init(this.campaignId, condition.restrict); }; SocialEvaluator.prototype.evaluate = function (type) { if (type === 'precondition') { return this.evaluatePrecondition(); } else if (type === 'condition') { return this.evaluateCondition(); } return false; }; SocialEvaluator.prototype.evaluatePrecondition = function () { const productData = STM_Util.hosting.getProductData(location.href); if (!productData.productId) { return false; } if (!this.intervalEvaluator.evaluate()) { return false; } return true; }; SocialEvaluator.prototype.evaluateCondition = function () { if (!this.restrictEvaluator.evaluate()) { return false; } return true; }; function ProductSearchEvaluator({ creative }) { /** @type {CreativeBase} */ this.creative = creative; } ProductSearchEvaluator.prototype.evaluate = function (type) { if (type === 'precondition') { return this.evaluatePrecondition(); } else if (type === 'condition') { return this.evaluateCondition(); } return false; }; ProductSearchEvaluator.prototype.evaluatePrecondition = function () { if (!this.creative.evaluate('precondition')) { return false; } return true; }; ProductSearchEvaluator.prototype.evaluateCondition = function () { if (!this.creative.evaluate('condition')) { return false; } return true; }; function RestrictEvaluator({ userCacheManager, observerManager, onsiteManager }) { /** @type {UserCacheManager} */ this.userCacheManager = userCacheManager; /** @type {ObserverManager} */ this.observerManager = observerManager; /** @type {OnsiteManager} */ this.onsiteManager = onsiteManager; this.restrictGroups = []; this.groupOperators = []; } RestrictEvaluator.prototype.init = function (groupConfig) { if (!Array.isArray(groupConfig)) { return; } for (const config of groupConfig) { const groupOrder = config.group_order; if (!this.restrictGroups[groupOrder]) { const groupOperator = config.group_operator; this.restrictGroups[groupOrder] = new RestrictGroup({ userCacheManager: this.userCacheManager, observerManager: this.observerManager, onsiteManager: this.onsiteManager, }); this.groupOperators[groupOrder] = groupOperator; } this.restrictGroups[groupOrder].add(config); } }; RestrictEvaluator.prototype.evaluate = function () { let flag = STM_Util.condition.evaluateGroup(this.restrictGroups, this.groupOperators); return flag; }; function RestrictGroup({ userCacheManager, observerManager, onsiteManager }) { /** @type {UserCacheManager} */ this.userCacheManager = userCacheManager; /** @type {ObserverManager} */ this.observerManager = observerManager; /** @type {OnsiteManager} */ this.onsiteManager = onsiteManager; this.restricts = []; this.operators = []; } RestrictGroup.prototype.add = function (config) { const option = config.option; const order = config.order; const operator = config.operator; const restrict = config.restrict; this.restricts[order] = this.getCondition(option, restrict); this.operators[order] = operator; }; RestrictGroup.prototype.getCondition = function (option, config) { let condition = new ConditionBase(); const dependency = { userCacheManager: this.userCacheManager, observerManager: this.observerManager, onsiteManager: this.onsiteManager, }; switch (config.how.depth1) { case 'visit': condition = new VisitCondition(dependency); break; case 'buy': condition = new BuyCondition(dependency); break; case 'cart': condition = new CartCondition(dependency); break; case 'coupon': condition = new CouponCondition(dependency); break; case 'campaign': condition = new CampaignCondition(dependency); break; case 'user': condition = new ClientCondition(dependency); break; } condition.init(option, config); return condition; }; RestrictGroup.prototype.evaluate = function () { let flag = STM_Util.condition.evaluateGroup(this.restricts, this.operators); return flag; }; function ConditionBase() { } ConditionBase.prototype.init = function () { }; ConditionBase.prototype.request = function () { }; ConditionBase.prototype.response = function () { }; ConditionBase.prototype.evaluate = function () { return false; }; function IntervalCondition({ userCacheManager, observerManager, onsiteManager, creative }) { /** @type {UserCacheManager} */ this.userCacheManager = userCacheManager; /** @type {ObserverManager} */ this.observerManager = observerManager; /** @type {OnsiteManager} */ this.onsiteManager = onsiteManager; /** @type {CreativeBase} */ this.creative = creative; this.campaignId = null; this.creativeType = null; this.hasViewLimit = false; this.viewLimit = null; this.hasClickInterval = false; this.hasViewInterval = false; this.campaignIntervalConfig = { click: { type: null, value: null }, view: { type: null, value: null }, }; this.ignoreOnsiteInterval = true; this.ignoreCouponInterval = true; this.isActiveOnsiteInterval = false; this.onsiteConfig = { interval: { type: null, value: 0 }, couponInterval: { type: null, value: 0 }, }; this.onsiteInterval = { popup: { seq: null, last: null }, frame: { seq: null, last: null }, coupon: { last: null } }; this.campaignInterval = {}; } IntervalCondition.prototype = Object.create(ConditionBase.prototype); IntervalCondition.prototype.constructor = IntervalCondition; IntervalCondition.prototype.init = function (campaignId, creativeType, setting) { this.campaignId = campaignId; this.creativeType = creativeType; this.hasViewLimit = !Number(setting.is_use_view_count); this.viewLimit = Number(setting.max_view_count); this.hasClickInterval = setting.interval.click.is_use; this.campaignIntervalConfig.click.type = setting.interval.click.date_type; this.campaignIntervalConfig.click.value = setting.interval.click.value; this.hasViewInterval = setting.interval.view.is_use; this.campaignIntervalConfig.view.type = setting.interval.view.date_type; this.campaignIntervalConfig.view.value = setting.interval.click.value; this.ignoreOnsiteInterval = Number(setting.is_use_ignore_interval); this.ignoreCouponInterval = Number(setting.is_use_coupon_interval); this.request(); }; IntervalCondition.prototype.request = function () { if (this.hasViewLimit || this.hasClickInterval || this.hasViewInterval) { this.onsiteManager.registerRequestData(OnsiteManager.REQUEST_TYPE.INTERVAL.CAMPAIGN, this.campaignId); } this.observerManager.subscribe(this.response.bind(this)); }; IntervalCondition.prototype.response = function (type, data) { if (type !== OnsiteManager.EVENT_RESPONSE) { return; } if (data.onsiteIntervalSetting) { const setting = data.onsiteIntervalSetting[this.creativeType]; this.isActiveOnsiteInterval = true; this.onsiteConfig.interval = setting.interval; this.onsiteConfig.couponInterval = setting.coupon_interval; } if (stmParams.isMember) { this.onsiteInterval = data.onsiteInterval; this.campaignInterval = data.campaignInterval?.[this.campaignId]; } else { const stored = this.userCacheManager.getDataStorage(); this.onsiteInterval = stored.get('onsite_interval'); this.campaignInterval = stored.get('campaign_interval')[this.campaignId]; } }; IntervalCondition.prototype.evaluate = function () { if (this.hasViewLimit && this.campaignInterval && !this.evaluateViewCount()) { return false; } if (this.hasClickInterval && this.campaignInterval && !this.evaluateDate('click')) { return false; } if (this.hasViewInterval && this.campaignInterval && !this.evaluateDate('view')) { return false; } if (this.isActiveOnsiteInterval && !this.ignoreOnsiteInterval && !this.evaluateOnsiteInterval()) { return false; } if (this.isActiveOnsiteInterval && this.creative.hasCoupon() && !this.ignoreCouponInterval && !this.evaluateCouponInterval()) { return false; } return true; }; IntervalCondition.prototype.evaluateViewCount = function () { const count = this.campaignInterval.count; return this.viewLimit > count; }; IntervalCondition.prototype.evaluateDate = function (type) { const lastDate = this.campaignInterval[type]; const { type: configType, value: configValue } = this.campaignIntervalConfig[type]; return this.checkInterval(lastDate, configType, configValue); }; IntervalCondition.prototype.evaluateOnsiteInterval = function () { const onsiteInterval = this.onsiteInterval[this.creativeType]; const campaignSeq = onsiteInterval?.seq; const lastDate = onsiteInterval?.last; const { type, value } = this.onsiteConfig.interval; if (String(campaignSeq) === String(this.campaignId)) { return true; } return this.checkInterval(lastDate, type, value); }; IntervalCondition.prototype.evaluateCouponInterval = function () { const couponInterval = this.onsiteInterval.coupon; const lastDate = couponInterval?.last; const { type, value } = this.onsiteConfig.couponInterval; return this.checkInterval(lastDate, type, value); }; IntervalCondition.prototype.checkInterval = function (lastDate, type, value) { if (!lastDate) { return true; } const now = Date.now(); switch (type) { case 'days': { const diffDays = STM_Util.date.getDiffInDay(lastDate, now); return diffDays >= value; } case 'hours': { const diffHours = STM_Util.date.getDiffInHour(lastDate, now); return diffHours > value; } case 'minutes': { const diffMinutes = STM_Util.date.getDiffInMinute(lastDate, now); return diffMinutes > value; } } return false; }; function SocialIntervalCondition({ userCacheManager }) { /** @type {UserCacheManager} */ this.userCacheManager = userCacheManager; this.campaignId = null; this.viewLimit = null; } SocialIntervalCondition.prototype = Object.create(ConditionBase.prototype); SocialIntervalCondition.prototype.constructor = SocialIntervalCondition; SocialIntervalCondition.prototype.init = function (campaignId, setting) { this.campaignId = campaignId; this.viewLimit = Number(setting.max_view_count); }; SocialIntervalCondition.prototype.evaluate = function () { const session = this.userCacheManager.getSession().get(); const sessionViewCount = session.onsite_show?.[this.campaignId] ?? 0; return this.viewLimit > sessionViewCount; }; function RestrictCondition({ userCacheManager, observerManager, onsiteManager }) { /** @type {UserCacheManager} */ this.userCacheManager = userCacheManager; /** @type {ObserverManager} */ this.observerManager = observerManager; /** @type {OnsiteManager} */ this.onsiteManager = onsiteManager; this.option = ''; this.depth1 = ''; this.depth2 = ''; this.depth2Type = ''; this.date = { min: '', max: '', liquidMin: '', liquidMax: '', session: '' }; this.item = { min: '', max: '', value: '', utmType: '' }; this.detail = { pages: [], itemType: '', items: [], itemFilter: [], categories: [], paths: [], isOr: false, }; }; RestrictCondition.prototype = Object.create(ConditionBase.prototype); RestrictCondition.prototype.constructor = RestrictCondition; RestrictCondition.prototype.init = function (option, config) { this.option = option; this.depth1 = config.how.depth1; this.depth2 = config.how.depth2; this.depth2Type = config.how.depth2_type; this.date = { min: '', max: '', liquidMin: '', liquidMax: '', session: '' }; this.item = { min: '', max: '', value: '', utmType: '' }; this.detail = { pages: [], itemType: '', items: [], itemFilter: [], categories: [], paths: [], isOr: false, }; this.initDate(config.how.date); this.initItem(config.how.item); this.initDetail(config.what.depth1); this.request(); }; RestrictCondition.prototype.initDate = function (config) { const { min, max, liquidMin, liquidMax, session } = config ?? {}; if (min || max) { this.date.min = min ?? null; this.date.max = max ?? null; } if (liquidMin || liquidMax) { this.date.liquidMin = liquidMin ? liquidMin : null; this.date.liquidMax = liquidMax ? liquidMax : null; this.date.min = liquidMin ? STM_Util.date.getDate(liquidMin) : null; this.date.max = liquidMax ? STM_Util.date.getDate(liquidMax) : null; } this.date.session = session; if (this.depth1 === 'visit' && this.depth2 === 'none' && !this.date.max) { if (this.date.liquidMax === null || typeof this.date.liquidMax === 'undefined' || this.date.liquidMax < 1) { this.date.max = STM_Util.date.getDate(1); } } }; RestrictCondition.prototype.initItem = function (config) { const { min, max, value, utm_type } = config ?? {}; this.item.min = min ? Number(min) : null; this.item.max = max ? Number(max) : null; this.item.value = value; this.item.utmType = utm_type; }; RestrictCondition.prototype.initDetail = function (config) { const type = config.type; switch (type) { case 'page': { const item = config.item ?? ''; const category = config.category ?? ''; const pathKeywords = config.path_keyword ?? ''; this.detail.pages = config.page ?? []; this.detail.itemType = config.item_type; this.detail.items = item.split(',').filter(Boolean); this.detail.categories = category.split(',').filter(Boolean); this.detail.paths = pathKeywords .split(',') .map((value) => { return value.trim(); }) .filter(Boolean); break; } case 'item': case 'category': { const item = config.item ?? ''; this.detail.itemType = type; this.detail.items = item.split(',').filter(Boolean); break; } } this.detail.isOr = Boolean(config.isOr); }; RestrictCondition.prototype.request = function () { let hasRequest = false; if (this.option) { this.onsiteManager.registerRequestData(OnsiteManager.REQUEST_TYPE.OPTION, this.option); hasRequest = true; } if (this.depth1 && !this.date.session) { this.onsiteManager.registerRequestData(OnsiteManager.REQUEST_TYPE.CACHE_TYPE, this.depth1); hasRequest = true; } if (this.detail.itemType === 'category') { this.onsiteManager.registerRequestData(OnsiteManager.REQUEST_TYPE.ITEM.CATEGORY, true); hasRequest = true; } if (hasRequest) { this.observerManager.subscribe(this.response.bind(this)); } }; RestrictCondition.prototype.response = function (type, data) { if (type !== OnsiteManager.EVENT_RESPONSE) { return; } this.detail.itemFilter = data.filterData?.[this.option] ?? []; }; RestrictCondition.prototype.inRange = function (type, value) { if (value === null || typeof value === 'undefined') { return false; } let min, max; if (type === 'date') { min = this.date.min ? new Date(this.date.min).getTime() : null; max = this.date.max ? new Date(this.date.max).getTime() : null; value = new Date(value).getTime(); } else if (type === 'num') { min = this.item.min; max = this.item.max; } else { return false; } if (min !== null && value < min) { return false; }; if (max !== null && value > max) { return false; }; return true; }; RestrictCondition.prototype.inDetail = function (type, value) { switch (type) { case 'page': return this.inPageDetail(value); case 'item': if (!this.inPageDetail('sq_detail_page')) { return false; } return this.inItemDetail(value); case 'category': if (!this.inPageDetail('sq_product_list_page')) { return false; } return this.inCategoryDetail(value); case 'path': return this.inPathDetail(); } return false; }; RestrictCondition.prototype.inPageDetail = function (curPage) { const isAll = this.detail.pages.length < 1; if (isAll) { return true; } const isIncluded = this.detail.pages.some((page) => { return String(page) === String(curPage); }); return isIncluded; }; RestrictCondition.prototype.inItemDetail = function (curItemId) { const isAll = this.detail.items.length < 1; const hasItemFilter = this.detail.itemFilter.length > 0; if (isAll && !hasItemFilter) { return true; } if (this.detail.itemType === 'item') { const isIncluded = this.detail.items.some((itemId) => { return String(itemId) === String(curItemId); }); return isIncluded; } else if (this.detail.itemType === 'category') { const isInFilter = this.detail.itemFilter.some((detail) => { return String(detail) === String(curItemId); }); if (hasItemFilter && !isInFilter) { return false; } let itemCategory = stmParams.itemCategory[curItemId]; if (!itemCategory || typeof itemCategory !== 'string') { return false; } itemCategory = itemCategory.split(','); const isIncluded = this.detail.items.some((categoryId) => { const isIncluded = itemCategory.some((itemCategoryId) => { return String(itemCategoryId) === String(categoryId); }); return isIncluded; }); return isIncluded; } return false; }; RestrictCondition.prototype.inCategoryDetail = function (curCategoryId) { const isAll = this.detail.categories.length < 1; if (isAll) { return true; } const isIncluded = this.detail.categories.some((categoryId) => { return String(categoryId) === String(curCategoryId); }); return isIncluded; }; RestrictCondition.prototype.inPathDetail = function () { const isAll = this.detail.paths.length < 1; if (isAll) { return true; } const href = decodeURIComponent(location.href); const isIncluded = this.detail.paths.some((path) => { return href.includes(path); }); return isIncluded; }; RestrictCondition.prototype.mergeData = function (target, source) { for (let key of Object.keys(source)) { let value = source[key]; if (typeof value === 'object' && !Array.isArray(value) && value !== null) { if (typeof target[key] === 'undefined') { target[key] = {}; } this.mergeData(target[key], value); } else if (typeof value === 'number' || !isNaN(Number(value))) { value = Number(value); switch (key) { case 'price': target[key] = value; break; default: target[key] = (target[key] || 0) + value; break; } } else { target[key] = value; } } }; function VisitCondition(dependency) { RestrictCondition.call(this, dependency); } VisitCondition.prototype = Object.create(RestrictCondition.prototype); VisitCondition.prototype.constructor = VisitCondition; VisitCondition.prototype.evaluate = function () { switch (this.depth2) { case 'count': if (this.detail.isOr) { return this.evaluateCountOr(); } return this.evaluateCount(); case 'begin': return this.evaluateBegin(); case 'again': return this.evaluateAgain(); case 'none': return this.evaluateNone(); case 'visit_ad': return this.evaluateAd(); case 'visit_url': switch (this.depth2Type) { case 'ref_url': return this.evaluateRef(); case 'param': return this.evaluateParam(); } return false; case 'n_query': return this.evaluateNQuery(); default: return false; } }; VisitCondition.prototype.evaluateCount = function () { const visit = this.getUserCache('inDateRange'); const total = this.getVisitCount(visit); return this.inRange('num', total); }; VisitCondition.prototype.evaluateCountOr = function () { const visit = this.getUserCache('inDateRange'); let result = false; for (const page of Object.keys(visit)) { if (page === 'item' || page === 'category') { result = Object.values(visit[page]).some((count) => { return this.inRange('num', Number(count)); }); } else { result = this.inRange('num', Number(visit[page])); } if (result) { break; } } return result; }; VisitCondition.prototype.evaluateBegin = function () { const visit = this.getUserCache('overall'); const total = this.getVisitCount(visit); return total === 1; }; VisitCondition.prototype.evaluateAgain = function () { const lastVisit = this.getUserCache('lastVisit'); if (lastVisit === null || typeof lastVisit === 'undefined') { return false; } return this.inRange('date', lastVisit); }; VisitCondition.prototype.evaluateNone = function () { const visit = this.getUserCache('inDateRange'); const total = this.getVisitCount(visit); return total === 0; }; VisitCondition.prototype.evaluateAd = function () { const searchParams = new URLSearchParams(location.search); const queryValue = (searchParams.get(this.item.utmType) || '').trim(); const targetUtms = (this.item.value ?? '') .split(',') .map((value) => { return value.trim(); }) .filter(Boolean); return targetUtms.includes(queryValue); }; VisitCondition.prototype.evaluateRef = function () { const domain = this.item.value; return document.referrer.startsWith(domain); }; VisitCondition.prototype.evaluateParam = function () { const href = decodeURIComponent(location.href); const paths = this.item.value .split(',') .map((value) => { return value.trim(); }) .filter(Boolean); return paths.some((path) => { return href.includes(path); }); }; VisitCondition.prototype.evaluateNQuery = function () { const searchParams = new URLSearchParams(location.search); const nQuery = (searchParams.get('n_query') || '').trim(); const keywords = (this.item.value ?? '') .split(',') .map((value) => { return value.trim(); }) .filter(Boolean); return keywords.includes(nQuery); }; VisitCondition.prototype.getUserCache = function (range) { let userCache = {}; switch (range) { case 'overall': for (const value of Object.values(this.userCacheManager.getDataStorage().get('visit'))) { this.mergeData(userCache, value); } userCache = this.filterDetail(userCache); break; case 'inDateRange': { if (this.date.session) { userCache = this.userCacheManager.getSession().get('visit'); } else { userCache = this.userCacheManager.getDataStorage().get('visit'); const filtered = {}; for (const date in userCache) { if (this.inRange('date', date)) this.mergeData(filtered, userCache[date]); } userCache = filtered; } userCache = this.filterDetail(userCache); break; } case 'lastVisit': { userCache = this.userCacheManager.getDataStorage().get('visit'); userCache = Object.keys(userCache).sort((a, b) => { return new Date(b).getTime() - new Date(a).getTime(); }); userCache = userCache[1]; break; } } return userCache; }; VisitCondition.prototype.filterDetail = function (userCache) { const filtered = {}; for (const page in userCache) { if (page === 'item' || page === 'category') { const data = userCache[page]; const result = {}; for (const id in data) { if (this.inDetail(page, id)) { result[id] = data[id]; } } if (Object.keys(result).length > 0) { filtered[page] = result; } } else { if (this.inDetail('page', page)) { filtered[page] = userCache[page]; } } } return filtered; }; VisitCondition.prototype.getVisitCount = function (userCache) { if (userCache === null || typeof userCache === 'undefined') { return 0; } let visitCount = 0; for (const page of Object.keys(userCache)) { if (page === 'item' || page === 'category') { for (const count of Object.values(userCache[page])) { visitCount += Number(count); } } else { visitCount += Number(userCache[page]); } } return visitCount; }; BuyCondition.MEMBER_ORDER_STATE = ['order', 'complete']; BuyCondition.NONMEMBER_ORDER_STATE = ['order']; function BuyCondition(dependency) { RestrictCondition.call(this, dependency); } BuyCondition.prototype = Object.create(RestrictCondition.prototype); BuyCondition.prototype.constructor = BuyCondition; BuyCondition.prototype.evaluate = function () { switch (this.depth2) { case 'order': if (this.detail.isOr) { return this.evaluateOrderOr(); } return this.evaluateOrder(); case 'option': return this.evaluateOption(); case 'ea': if (this.detail.isOr) { return this.evaluateEaOr(); } return this.evaluateEa(); case 'price': if (this.detail.isOr) { return this.evaluatePriceOr(); } return this.evaluatePrice(); case 'nopay': return this.evaluateNopay(); case 'return': if (this.detail.isOr) { return this.evaluateStatusOr('return'); } return this.evaluateStatus('return'); case 'cancel': if (this.detail.isOr) { this.evaluateStatusOr('cancel'); } return this.evaluateStatus('cancel'); case 'trade': if (this.detail.isOr) { return this.evaluateStatusOr('trade'); } return this.evaluateStatus('trade'); case 'nobuy': if (this.detail.isOr) { return this.evaluateNobuyOr(); } return this.evaluateNobuy(); case 'last_buy_after': if (this.detail.isOr) { return this.evaluateLastBuyAfterOr(); } return this.evaluateLastBuyAfter(); case 'total_order_count': return this.evaluateTotalOrderCount(); case 'total_price': return this.evaluateTotalPrice(); default: return false; } }; BuyCondition.prototype.evaluateOrder = function () { const mainOrders = this.getUserCache('mainOrders', 'inDateRange'); const count = Object.keys(mainOrders).length; return this.inRange('num', count); }; BuyCondition.prototype.evaluateOrderOr = function () { const mainOrders = this.getUserCache('mainOrders', 'inDateRange'); const items = {}; for (const mainOrder of Object.values(mainOrders)) { const itemSet = new Set(); for (const order of mainOrder.orders) { const itemId = order.item_id; if (itemSet.has(itemId)) { continue; } else { itemSet.add(itemId); } items[itemId] = (items[itemId] ?? 0) + 1; } } const result = Object.values(items).some((count) => { return this.inRange('num', count); }); return result; }; BuyCondition.prototype.evaluateOption = function () { const orders = this.getUserCache('orders', 'inDateRange'); const count = Object.keys(orders).length; return this.inRange('num', count); }; BuyCondition.prototype.evaluateEa = function () { const orders = this.getUserCache('orders', 'inDateRange'); let count = 0; for (const order of Object.values(orders)) { count += order.order_cnt; } return this.inRange('num', count); }; BuyCondition.prototype.evaluateEaOr = function () { const orders = this.getUserCache('orders', 'inDateRange'); const result = Object.values(orders).some((order) => { return this.inRange('num', order.order_cnt); }); return result; }; BuyCondition.prototype.evaluatePrice = function () { const mainOrders = this.getUserCache('mainOrders', 'inDateRange'); let price = 0; for (const mainOrder of Object.values(mainOrders)) { const isAll = this.detail.items.length < 1 && this.detail.itemFilter.length < 1; if (isAll) { price += Number(mainOrder.payed_price); } else { for (const order of mainOrder.orders) { price += Number(order.sale_price) * Number(order.order_cnt); } } } return this.inRange('num', price); }; BuyCondition.prototype.evaluatePriceOr = function () { const mainOrders = this.getUserCache('mainOrders', 'inDateRange'); const items = {}; for (const mainOrder of Object.values(mainOrders)) { for (const order of mainOrder.orders) { const itemId = order.item_id; const price = Number(order.sale_price); const count = Number(order.order_cnt); items[itemId] = (items[itemId] ?? 0) + (price * count); } } const result = Object.values(items).some((price) => { return this.inRange('num', price); }); return result; }; BuyCondition.prototype.evaluateNopay = function () { const mainOrders = this.getUserCache('mainOrders', 'inDateRange', 'nopay'); const count = Object.keys(mainOrders).length; return count > 0; }; BuyCondition.prototype.evaluateStatus = function (status) { const orders = this.getUserCache('orders', 'inDateRange', status); const count = Object.keys(orders).length; return this.inRange('num', count); }; BuyCondition.prototype.evaluateStatusOr = function (status) { const orders = this.getUserCache('orders', 'inDateRange', status); const items = {}; for (const itemId of Object.keys(orders)) { items[itemId] = (items[itemId] ?? 0) + 1; } const result = Object.values(items).some((count) => { return this.inRange('num', count); }); return result; }; BuyCondition.prototype.evaluateNobuy = function () { const mainOrders = this.getUserCache('mainOrders', 'inDateRange', 'none'); const count = Object.keys(mainOrders).length; return count === 0; }; BuyCondition.prototype.evaluateNobuyOr = function () { const mainOrders = this.getUserCache('mainOrders', 'inDateRange', 'none'); const itemSet = new Set(); for (const mainOrder of Object.values(mainOrders)) { const orders = mainOrder.orders; for (const order of orders) { itemSet.add(order.item_id); } } const result = this.detail.items.some((itemId) => { return !itemSet.has(itemId); }); return result; }; BuyCondition.prototype.evaluateLastBuyAfter = function () { const orders = this.getUserCache('lastOrder'); const lastest = orders[0]; if (lastest === null || typeof lastest === 'undefined') { return false; } return this.inRange('date', lastest.date); }; BuyCondition.prototype.evaluateLastBuyAfterOr = function () { const orders = this.getUserCache('lastOrder'); const result = orders.some((order) => { return this.inRange('date', order.date); }); return result; }; BuyCondition.prototype.evaluateTotalOrderCount = function () { const mainOrders = this.getUserCache('mainOrders', 'overall', 'none'); const count = Object.keys(mainOrders).length; return this.inRange('num', count); }; BuyCondition.prototype.evaluateTotalPrice = function () { const mainOrders = this.getUserCache('mainOrders', 'overall', 'none'); let price = 0; for (const mainOrder of Object.values(mainOrders)) { if (this.item.value === 'related_product') { price += Number(mainOrder.payed_price); } else { price += Number(mainOrder.total_price); } } return this.inRange('num', price); }; BuyCondition.prototype.getUserCache = function (type, range, status) { let userCache = {}; switch (type) { case 'mainOrders': // 날짜 필터 switch (range) { case 'overall': userCache = this.userCacheManager.getDataStorage().get('order'); break; case 'inDateRange': if (this.date.session) { userCache = this.userCacheManager.getSession().get('order'); } else { userCache = this.userCacheManager.getDataStorage().get('order'); userCache = Object.fromEntries( Object.entries(userCache).filter(([orderNum, mainOrder]) => { const inRange = mainOrder.orders.some((order) => { return this.inRange('date', order.date); }); return inRange; }) ); } break; } // 세부조건 필터 userCache = this.filterDetail('mainOrders', userCache); // 상태 필터 if (status !== 'none') { let targetStatus = []; if (status) { targetStatus.push(status); } else { if (stmParams.isMember) { targetStatus = BuyCondition.MEMBER_ORDER_STATE; } else { targetStatus = BuyCondition.NONMEMBER_ORDER_STATE; } } const statusFiltered = {}; for (const key of Object.keys(userCache)) { const mainOrder = userCache[key]; const orders = mainOrder.orders.filter((order) => { return targetStatus.includes(order.status); }); if (orders.length > 0) { mainOrder.orders = orders; statusFiltered[key] = mainOrder; } } userCache = statusFiltered; } break; case 'orders': // 날짜 필터 switch (range) { case 'overall': userCache = this.userCacheManager.getDataStorage().get('order'); break; case 'inDateRange': if (this.date.session) { userCache = this.userCacheManager.getSession().get('order'); } else { userCache = this.userCacheManager.getDataStorage().get('order'); } break; } const orderRows = []; Object.values(userCache ?? {}).forEach((mainOrder) => { (mainOrder?.orders ?? []).forEach((order) => { orderRows.push(order); }); }); userCache = orderRows; // 세부조건 필터 userCache = this.filterDetail('orders', userCache); // 날짜 필터 if (range = 'inDateRange') { userCache = userCache.filter((order) => { return this.inRange('date', order.date); }); } // 상태 필터 if (status !== 'none') { let targetStatus = []; if (status) { targetStatus.push(status); } else { if (stmParams.isMember) { targetStatus = BuyCondition.MEMBER_ORDER_STATE; } else { targetStatus = BuyCondition.NONMEMBER_ORDER_STATE; } } userCache = userCache.filter((order) => { return targetStatus.includes(order.status); }); } // 병합 const merged = {}; for (const order of userCache) { if (!merged[order.item_id]) { merged[order.item_id] = { cate_id: order.cate_id, order_cnt: 0, sale_price: 0, }; } const curOrder = merged[order.item_id]; curOrder.order_cnt += Number(order.order_cnt); curOrder.sale_price += Number(order.sale_price); } userCache = merged; break; case 'lastOrder': userCache = this.userCacheManager.getDataStorage().get('order') ?? {}; const lastOrderRows = []; Object.values(userCache ?? {}).forEach((mainOrder) => { (mainOrder?.orders ?? []).forEach((order) => { lastOrderRows.push(order); }); }); userCache = lastOrderRows; // 세부조건 필터 userCache = this.filterDetail('orders', userCache); // 상태 필터 userCache = userCache.filter((order) => { return BuyCondition.MEMBER_ORDER_STATE.includes(order.status); }); userCache = userCache.sort((a, b) => { return new Date(b.date).getTime() - new Date(a.date).getTime(); }); const itemSet = new Set(); const lastOrder = []; for (const order of userCache) { const itemId = order.item_id; if (itemSet.has(itemId)) { continue; } else { itemSet.add(itemId); } lastOrder.push(order); } userCache = lastOrder; break; }; return userCache; }; BuyCondition.prototype.filterDetail = function (type, userCache) { let filtered = {}; switch (type) { case 'mainOrders': { for (const orderNum in userCache) { const mainOrder = userCache[orderNum]; const filteredOrders = mainOrder.orders.filter((order) => { return this.inDetail('item', order.item_id); }); if (filteredOrders.length > 0) { mainOrder.orders = filteredOrders; filtered[orderNum] = mainOrder; } } break; } case 'orders': { filtered = userCache.filter((order) => { return this.inDetail('item', order.item_id); }); break; } } return filtered; }; function CartCondition(dependency) { RestrictCondition.call(this, dependency); } CartCondition.prototype = Object.create(RestrictCondition.prototype); CartCondition.prototype.constructor = CartCondition; CartCondition.prototype.evaluate = function () { switch (this.depth2) { case 'option': return this.evaluateOption(); case 'ea': if (this.detail.isOr) { return this.evaluateEaOr(); } return this.evaluateEa(); case 'nobuy': return this.evaluateNobuy(); case 'total_price': return this.evaluateTotalPrice(); default: return false; } }; CartCondition.prototype.evaluateOption = function () { const history = this.getUserCache('add'); let count = 0; for (const cart of Object.values(history)) { count += Object.keys(cart).length; } return this.inRange('num', count); }; CartCondition.prototype.evaluateEa = function () { const history = this.getUserCache('add'); let count = 0; for (const cart of Object.values(history)) { for (const item of Object.values(cart)) { count += item.count; } } return this.inRange('num', count); }; CartCondition.prototype.evaluateEaOr = function () { const history = this.getUserCache('add'); const items = {}; for (const cart of Object.values(history)) { for (const itemId of Object.keys(cart)) { const count = cart[itemId].count; items[itemId] = (items[itemId] ?? 0) + count; } } const result = Object.values(items).some((count) => { return this.inRange('num', count); }); return result; }; CartCondition.prototype.evaluateNobuy = function () { const cart = this.getUserCache('last'); const count = Object.keys(cart).length; return this.inRange('num', count); }; CartCondition.prototype.evaluateTotalPrice = function () { const cart = this.getUserCache('last'); const items = Object.values(cart); if (items.length === 0) { return false; } let price = 0; for (const item of items) { price += (item.total_price ?? Number(item.sale_price ?? item.price) * Number(item.count)); } return this.inRange('num', price); }; CartCondition.prototype.getUserCache = function (type) { let userCache = {}; if (this.date.session) { userCache = this.userCacheManager.getSession().get('cart'); if (type === 'add') { userCache = this.getAddHistory(userCache, 'session'); } } else { userCache = this.userCacheManager.getDataStorage().get('cart'); if (type === 'add') { userCache = this.getAddHistory(userCache); } const filtered = {}; for (const date in userCache) { if (this.inRange('date', date)) filtered[date] = userCache[date]; } userCache = filtered; } userCache = this.filterDetail(type, userCache); if (type === 'last') { const sortedDates = Object.keys(userCache).sort((a, b) => { return b - a; }); const latestDate = sortedDates[0]; userCache = userCache[latestDate] || {}; } return userCache; }; CartCondition.prototype.getAddHistory = function (userCache, type) { const result = {}; const sortedDates = Object.keys(userCache).sort(); let lastCart = {}; if (type === 'session') { lastCart = userCache[sortedDates[0]]; } for (let i = 0; i < sortedDates.length; i++) { const date = sortedDates[i]; const dayData = userCache[date]; const increased = {}; for (const id in dayData) { const item = dayData[id]; const prev = lastCart[id] && lastCart[id].count ? lastCart[id].count : 0; const curr = Number(item.count) || 0; const diff = curr - prev; if (diff > 0) { increased[id] = { count: diff }; } else if (diff < 0 && curr !== 0) { increased[id] = { count: curr }; } } if (Object.keys(increased).length > 0) { result[date] = increased; } lastCart = dayData; } return result; }; CartCondition.prototype.filterDetail = function (type, userCache) { let filtered = {}; for (const date of Object.keys(userCache)) { const cart = userCache[date]; const filteredCart = {}; for (const itemId of Object.keys(cart)) { if (this.inDetail('item', itemId)) { filteredCart[itemId] = cart[itemId]; } } if (type === 'add' && Object.keys(filteredCart).length === 0) { continue; } filtered[date] = filteredCart; } return filtered; }; function CouponCondition(dependency) { RestrictCondition.call(this, dependency); } CouponCondition.prototype = Object.create(RestrictCondition.prototype); CouponCondition.prototype.constructor = CouponCondition; CouponCondition.prototype.evaluate = function () { switch (this.depth2) { case 'receive': return this.evaluateReceive(); case 'not_receive': return !this.evaluateReceive(); case 'use': return this.evaluateReceive() && this.evaluateUse(); case 'receive_not_use': return this.evaluateReceive() && !this.evaluateUse(); default: return false; } }; CouponCondition.prototype.evaluateReceive = function () { const coupons = this.getUserCache(); const count = Object.keys(coupons).length; return count > 0; }; CouponCondition.prototype.evaluateUse = function () { const coupons = this.getUserCache(); let isUse = false; for (let history of Object.values(coupons)) { history = history.filter((record) => { return record.use.toLowerCase() === 'y'; }); if (history.length > 0) { isUse = true; } } return isUse; }; CouponCondition.prototype.getUserCache = function () { let userCache = {}; if (this.date.session) { userCache = this.userCacheManager.getSession().get('coupon'); } else { userCache = this.userCacheManager.getDataStorage().get('coupon'); const filtered = {}; for (const couponId of Object.keys(userCache)) { let history = userCache[couponId]; history = history.filter((record) => { return this.inRange('date', record.date); }); if (history.length > 0) { filtered[couponId] = history; } } userCache = filtered; } userCache = this.filterDetail(userCache); return userCache; }; CouponCondition.prototype.filterDetail = function (userCache) { const filtered = {}; const target = this.item.value.split(','); for (const couponId in userCache) { if (target.includes(couponId)) { filtered[couponId] = userCache[couponId]; } } return filtered; }; function CampaignCondition(dependency) { RestrictCondition.call(this, dependency); } CampaignCondition.prototype = Object.create(RestrictCondition.prototype); CampaignCondition.prototype.constructor = CampaignCondition; CampaignCondition.prototype.evaluate = function () { switch (this.depth2) { case 'receive': return this.evaluateReceive(); case 'not_receive': return !this.evaluateReceive(); case 'receive_click': return this.evaluateReceive() && this.evaluateClick(); case 'receive_not_click': return this.evaluateReceive() && !this.evaluateClick(); default: return false; } }; CampaignCondition.prototype.evaluateReceive = function () { const campaigns = this.getUserCache(); const count = Object.keys(campaigns).length; return count > 0; }; CampaignCondition.prototype.evaluateClick = function () { const campaigns = this.getUserCache(); let isClicked = false; for (let history of Object.values(campaigns)) { history = history.filter((record) => { return record.click.toLowerCase() === 'y'; }); if (history.length > 0) { isClicked = true; } } return isClicked; }; CampaignCondition.prototype.getUserCache = function () { let userCache = {}; userCache = this.userCacheManager.getDataStorage().get('campaign'); const filtered = {}; for (const campaignId of Object.keys(userCache)) { let history = userCache[campaignId]; history = history.filter((record) => { return this.inRange('date', record.date); }); if (history.length > 0) { filtered[campaignId] = history; } } userCache = filtered; userCache = this.filterDetail(userCache); return userCache; }; CampaignCondition.prototype.filterDetail = function (userCache) { const filtered = {}; const target = this.item.value.split(','); for (const campaignId of Object.keys(userCache)) { if (target.includes(campaignId)) { filtered[campaignId] = userCache[campaignId]; } } return filtered; }; function ClientCondition(dependency) { RestrictCondition.call(this, dependency); } ClientCondition.prototype = Object.create(RestrictCondition.prototype); ClientCondition.prototype.constructor = ClientCondition; ClientCondition.prototype.evaluate = function () { switch (this.depth2) { case 'join_date': return this.evaluateJoin(); case 'grade': return this.evaluateGrade(); case 'mileage': return this.evaluateMileage(); case 'marketing_agree': return this.evaluateMarketingAgree(); case 'kakao_friend': return this.evaluateKfreind(); case 'sleep_clear': return this.evaluateSleepClear(); case 'os': return this.evaluateOs(); case 'browser': return this.evaluateBrowser(); default: return false; } }; ClientCondition.prototype.evaluateJoin = function () { const client = this.getUserCache(); if (client.join === true) { return true; } return this.inRange('date', client.join_date); }; ClientCondition.prototype.evaluateGrade = function () { const client = this.getUserCache(); return this.item.value.includes(client.grade); }; ClientCondition.prototype.evaluateMileage = function () { const client = this.getUserCache(); const mileage = client.mileage; if (mileage === null || typeof mileage === 'undefined') { return false; } return this.inRange('num', Number(client.mileage)); }; ClientCondition.prototype.evaluateMarketingAgree = function () { const client = this.getUserCache(); if (this.item.value === 'agree') { return client.marketing_agree; } else { return !client.marketing_agree; } }; ClientCondition.prototype.evaluateKfreind = function () { const client = this.getUserCache(); if (this.item.value === 'friend') { return client.kakao_friend; } else { return !client.kakao_friend; } }; ClientCondition.prototype.evaluateSleepClear = function () { const client = this.getUserCache(); if (!client.sleep_clear) { return false; } return this.inRange('date', client.sleep_clear); }; ClientCondition.prototype.evaluateOs = function () { const os = STM_Util.navigator.getAgent().os; if (this.item.value.includes('windows') && os === 'W') { return true; } else if (this.item.value.includes('android') && os === 'A') { return true; } else if (this.item.value.includes('macOS') && os === 'M') { return true; } else if (this.item.value.includes('iOS') && os === 'I') { return true; } return false; }; ClientCondition.prototype.evaluateBrowser = function () { const browser = STM_Util.navigator.getAgent().browser; if (this.item.value.includes('chrome') && browser === 'C') { return true; } else if (this.item.value.includes('safari') && browser === 'S') { return true; } else if (this.item.value.includes('samsung_browser') && browser === 'SB') { return true; } else if (this.item.value.includes('edge') && (browser === 'E' || browser === 'EC')) { return true; } else if (this.item.value.includes('firefox') && browser === 'F') { return true; } else if (this.item.value.includes('whale') && browser === 'W') { return true; } else if (this.item.value.includes('naver') && browser === 'N') { return true; } else if (this.item.value.includes('kakao') && browser === 'K') { return true; } else if (this.item.value.includes('zigzag') && browser === 'Z') { return true; } else if (this.item.value.includes('avely') && browser === 'A') { return true; } else if (this.item.value.includes('byapps') && browser === 'B') { return true; } return false; }; ClientCondition.prototype.getUserCache = function () { let userCache = {}; if (this.date.session) { userCache = this.userCacheManager.getSession().get('client'); } else { userCache = this.userCacheManager.getDataStorage().get('client'); } return userCache; }; function SocialCondition(dependency) { RestrictCondition.call(this, dependency); this.creative = dependency.creative; this.campaignId = null; this.date = { min: '', max: '', liquidMin: '', liquidMax: '', session: '' }; this.dateType = ''; this.cache = { view: 0, basket: 0, buy: 0, with: {}, }; this.dateTagName = ''; this.countTagName = ''; } SocialCondition.prototype = Object.create(RestrictCondition.prototype); SocialCondition.prototype.constructor = SocialCondition; SocialCondition.prototype.init = function (campaignId, config) { RestrictCondition.prototype.init.call(this, null, config); this.campaignId = campaignId; this.dateType = config.how.date_type || config.how.date.date_type; if (this.depth2 === 'view') { this.dateTagName = 'set_day_view'; this.countTagName = 'view_count'; } else if (this.depth2 === 'basket') { this.dateTagName = 'set_day_cart'; this.countTagName = 'cart_count'; } else if (this.depth2 === 'buy') { this.dateTagName = 'set_day_purchase'; this.countTagName = 'purchase_count'; } else if (this.depth2 === 'with') { this.dateTagName = 'set_day_best'; this.countTagName = 'best_count'; } this.request(); }; SocialCondition.prototype.request = function () { let date = ''; switch (this.dateType) { case 'date': date = STM_Util.date.getDate(this.date.liquidMin) + ' 00:00:00'; break; case 'time': const subtractedDate = new Date(Date.now() - (this.date.liquidMin * HOUR * 1000)); const year = subtractedDate.getFullYear(); const month = String(subtractedDate.getMonth() + 1).padStart(2, '0'); const day = String(subtractedDate.getDate()).padStart(2, '0'); const hour = String(subtractedDate.getHours()).padStart(2, '0'); date = `${year}-${month}-${day} ${hour}:00:00`; break; } if (!date) { return; } this.onsiteManager.registerRequestData( OnsiteManager.REQUEST_TYPE.SOCIAL, { campaignSeq: this.campaignId, date: date, }, ); this.observerManager.subscribe(this.response.bind(this)); }; SocialCondition.prototype.response = function (type, data) { if (type !== OnsiteManager.EVENT_RESPONSE) { return; } const itemCache = data.item_cache?.[this.campaignId] ?? {}; this.cache.view = itemCache.view ?? 0; this.cache.basket = itemCache.basket ?? 0; this.cache.buy = itemCache.buy ?? 0; this.cache.with = itemCache.with ?? {}; this.creative.setSocialType(this.depth2); this.setTag(); this.setItem(); }; SocialCondition.prototype.setTag = function () { const date = this.date.liquidMin + (this.dateType === 'date' ? '일' : '시간'); let count = this.cache[this.depth2] ?? 0; if (this.depth2 === 'with') { count = Object.values(this.cache.with)[0]; } this.creative.setTag(this.dateTagName, date); this.creative.setTag(this.countTagName, count); }; SocialCondition.prototype.setItem = function () { if (this.depth2 !== 'with') { return; } const item = Object.keys(this.cache.with)[0]; this.creative.setSocialItem(item); }; SocialCondition.prototype.evaluate = function () { switch (this.depth2) { case 'view': return this.evaluateCount('view'); case 'basket': return this.evaluateCount('basket'); case 'buy': return this.evaluateCount('buy'); case 'with': return this.evaluateWith(); default: return false; } }; SocialCondition.prototype.evaluateCount = function (type) { const count = Number(this.cache[type]); return this.inRange('num', count); }; SocialCondition.prototype.evaluateWith = function () { const count = Object.values(this.cache.with)[0]; return this.inRange('num', count); }; /** * 디바이스별 배경 투명도 원본값을 해석한다. * is_popup_opacity_device_separate가 켜져 있으면 해당 디바이스 값(없으면 공통값)을, * 꺼져 있으면 공통값을 반환한다. 0(완전 투명)도 유효값이므로 || 가 아닌 ?? 로 병합한다. */ function resolveDeviceOpacityRaw(isSeparate, device, pcVal, moVal, baseVal) { if (!isSeparate) return baseVal; return device === 'pc' ? (pcVal ?? baseVal) : (moVal ?? baseVal); } function CreativeBase({ userCacheManager, observerManager, onsiteManager }) { /** @type {UserCacheManager} */ this.userCacheManager = userCacheManager; /** @type {ObserverManager} */ this.observerManager = observerManager; /** @type {OnsiteManager} */ this.onsiteManager = onsiteManager; /** @type {ContentBase[]} */ this.contents = {}; this.evaluator = new CreativeEvaluator({ observerManager, onsiteManager }); this.id = null; this.campaignId = null; this.type = null; this.templateSeq = null; this.contentCoupons = new Set(); this.target = null; this.iframe = null; this.triggerIframe = null; this.tag = {}; // 소셜프루프 this.socialType = ''; this.socialItem = null; // 팝업, 소셜프루프 노출 위치 this.position = null; this.positionMarginTop = null; this.positionMarginBottom = null; this.positionMarginLeft = null; this.positionMarginRight = null; // frame 노출 위치 this.displayPageSelector = null; this.displayPagePosition = null; // 배경 this.hasBackground = false; this.background = null; this.backgroundOpacity = 0; // 노출 지연 this.isDisplayDelay = null; this.displayDelaySecends = null; // 자동 종료 this.isAutoClose = null; this.autoCloseSeconds = null; // 인터렉션 this.hasInteraction = null; this.interactionType = null; this.interactionScrollPercent = null; this.interactionClickSelector = null; this.interactionHoverSelector = null; this.interactionHoverSeconds = null; this.isDisplayed = false; } CreativeBase.prototype.init = function (id, campaignId, type, config) { const device = STM_Util.navigator.getDeviceType(); this.id = id; this.campaignId = campaignId; this.type = type; this.templateSeq = config.template_seq; this.initContents(config.content); if (Object.keys(this.contents).length < 1) { return; } this.evaluator.init(this.type, config); // 소셜프루프 this.socialItem = STM_Util.hosting.getProductData(location.href).productId; // 팝업, 소셜 프루프 노출 위치 const position = config?.setting?.position[device] ?? {}; this.position = String(position.value); if (position['css']) { this.positionMarginTop = position['css']['margin-top'] ?? '0px'; this.positionMarginBottom = position['css']['margin-bottom'] ?? '0px'; this.positionMarginLeft = position['css']['margin-left'] ?? '0px'; this.positionMarginRight = position['css']['margin-right'] ?? '0px'; } else { this.positionMarginTop = '10px'; this.positionMarginBottom = '10px'; this.positionMarginLeft = '10px'; this.positionMarginRight = '10px'; } // frame 노출 위치 const displayPage = config?.setting?.display_page?.[device] ?? {}; this.displayPageSelector = displayPage.selector; this.displayPagePosition = displayPage.position; // 배경 const opacityRaw = resolveDeviceOpacityRaw( config.setting.is_popup_opacity_device_separate, device, config.setting.popup_background_opacity_pc, config.setting.popup_background_opacity_mo, config.setting.popup_background_opacity ); this.backgroundOpacity = Number(opacityRaw) / 100; this.hasBackground = !isNaN(Number(this.backgroundOpacity)) && this.backgroundOpacity > 0; // 노출 지연 this.isDisplayDelay = config.setting.is_display_delay; this.displayDelaySecends = Number(config.setting.display_delay_sec) * 1000; // 자동 종료 this.isAutoClose = config.setting.is_display_autoquit; this.autoCloseSeconds = Number(config.setting.display_autoquit_sec) * 1000; // 인터렉션 const interaction = config?.setting?.interaction ?? {}; this.hasInteraction = config.setting.is_use_interaction; this.interactionType = interaction.type; this.interactionScrollPercent = Number(device === 'pc' ? interaction.scroll?.pcScrollPercent : interaction.scroll?.mobileScrollPercent); this.interactionClickSelector = this.getInteractionSelector(interaction.click?.[device]); this.interactionHoverSelector = this.getInteractionSelector(interaction.hover?.[device]); this.interactionHoverSeconds = Number(interaction.hover?.hoverTime) * 1000; if (this.type === 'frame') { if (this.displayPageSelector) { this.target = document.querySelector(this.displayPageSelector); } } else { this.target = document.body; } }; CreativeBase.prototype.initContents = function (contentConfig) { for (const contentId of Object.keys(contentConfig)) { const content = new ContentBase({ userCacheManager: this.userCacheManager, observerManager: this.observerManager, onsiteManager: this.onsiteManager, creative: this, }); content.init(contentId, contentConfig[contentId]); if (content.evaluate()) { this.contents[contentId] = content; } } }; CreativeBase.prototype.getInteractionSelector = function (config) { let selectotr = ''; switch (config?.selectedClass) { case 'id': selectotr = '#' + config.id; break; case 'class': selectotr = '.' + config.class; break; } return selectotr; }; CreativeBase.prototype.hasCoupon = function () { const hasCoupon = Object.values(this.contents).some((content) => { return content.getCouponSeq(); }); return hasCoupon; }; CreativeBase.prototype.getIframe = function () { return this.iframe; }; CreativeBase.prototype.getDisplayPagePosition = function () { return this.displayPagePosition; }; CreativeBase.prototype.getTag = function () { return this.tag; }; CreativeBase.prototype.setTag = function (key, value) { this.tag[key] = value; }; CreativeBase.prototype.setSocialType = function (value) { this.socialType = value; }; CreativeBase.prototype.setSocialItem = function (item) { this.socialItem = item; }; CreativeBase.prototype.removeContent = function (contentSeq) { delete this.contents[contentSeq]; }; CreativeBase.prototype.evaluate = function (type) { if (Object.keys(this.contents).length < 1) { return false; } if (!this.onsiteManager.creativeEvaluate?.[this.id] && type === 'condition') { return false; } return this.evaluator.evaluate(type); }; CreativeBase.prototype.render = function () { if (this.iframe) { return; } this.iframe = this.createIframe(this.getIframeConfig(0)); this.insertIframe(this.iframe); }; CreativeBase.prototype.getIframeConfig = function (iframeIndex) { const config = { iframeIndex, type: 'onsite_view', token: STM_Util.storage.sdl.get(), measurementId: MEASUREMENT_ID, device: STM_Util.navigator.getDeviceType(), hosting: STM_Util.hosting.getHosting(), isMember: stmParams.isMember, su: STM_Util.storage.localStorage.get(SU), campaignId: this.campaignId, creativeId: this.id, contentIds: Object.keys(this.contents), creativeType: this.type, tag: this.tag, }; if (this.type === 'frame') { if (!this.target || !this.target.parentElement) { this.target = document.querySelector(this.displayPageSelector); } config.maxWidth = this.target?.parentElement?.offsetWidth; } else { config.maxWidth = this.target.offsetWidth; // freeform 템플릿 height 리사이징용: 부모 페이지의 실제 뷰포트 높이 전달 // (iframe 내부 window.innerHeight는 iframe 자체 크기라 디바이스 뷰포트와 다름) config.maxHeight = window.innerHeight - 20; } if (this.type === 'social') { config.social = {}; config.social.type = this.socialType; config.social.item = this.socialItem; } if (this.type === 'product_search') { const contents = Object.values(this.contents); config.contentIds = contents[iframeIndex].id; config.lastViewItemId = STM_Util.storage.localStorage.get(LAST_VIEW_ITEM) || null; } return config; }; CreativeBase.prototype.createIframe = function (config) { const iframe = document.createElement('iframe'); iframe.id = `snap_onsite_${this.campaignId}_${config.iframeIndex}`; iframe.src = BASE_CDN_URL; iframe.style.border = 'none'; iframe.style.width = '0px'; iframe.style.height = '0px'; iframe.style.position = 'relative'; iframe.style.zIndex = '101'; iframe.style.overflow = 'hidden'; iframe.addEventListener('load', async () => { iframe.contentWindow.postMessage(config, FRONT_URL); }); return iframe; }; CreativeBase.prototype.insertIframe = function (iframe) { switch (this.type) { case 'frame': this.target = document.querySelector(this.displayPageSelector); if (this.target) { switch (this.displayPagePosition) { case 'top': this.target.before(iframe); break; case 'bottom': this.target.after(iframe); break; } } break; default: this.target.appendChild(iframe); break; } }; CreativeBase.prototype.display = async function (config) { if (this.isDisplayed && !(this.type === 'product_search' && config.iframeIndex === 1)) { return; } else { this.isDisplayed = true; } if (this.isDisplayDelay) { await this.delayDisplay(); } switch (this.type) { case 'popup': this.setSize(config.width, config.height); this.iframe.style.zIndex = POPUP_Z_INDEX; this.displayPosition(this.iframe); if (this.hasBackground) { this.displayBackground(this.iframe, config.iframeIndex); } break; case 'frame': this.setSize('100%', config.height); break; case 'social': this.setSize(config.width, config.height); this.iframe.style.zIndex = SOCIAL_Z_INDEX; this.displayPosition(this.iframe); break; case 'product_search': if (config.iframeIndex === 0) { this.setSize(config.width, config.height); if (STM_Util.navigator.getDeviceType() !== 'pc') { this.iframe.style.width = 'calc(100% - 24px)'; } this.iframe.style.zIndex = POPUP_Z_INDEX; this.displayPosition(this.iframe); } else { if (STM_Util.navigator.getDeviceType() === 'pc') { Object.assign(this.triggerIframe.style, { width: config.width, height: config.height, }); this.triggerIframe.style.zIndex = POPUP_Z_INDEX; this.displayPosition(this.triggerIframe, { position: '5' }); this.backgroundOpacity = config.backgroundOpacity; this.displayBackground(this.triggerIframe, config.iframeIndex); } else { Object.assign(this.triggerIframe.style, { width: '100%', height: '100%', inset: '0', }); this.positionMarginBottom = '0px'; this.triggerIframe.style.zIndex = POPUP_Z_INDEX; this.displayPosition(this.triggerIframe, { position: '3', positionMarginBottom: 0 }); ScrollLock.lock(); } } break; } if (this.isAutoClose) { this.autoClose(); } }; CreativeBase.prototype.setSize = function (width, height) { // product_search MO: width는 display()에서 calc(100% - 24px)로 고정 — SIZE 메시지가 덮어쓰지 않도록 스킵 if (this.type === 'product_search' && STM_Util.navigator.getDeviceType() !== 'pc') { this.iframe.style.height = height; return; } Object.assign(this.iframe.style, { width: width, height: height, }); }; CreativeBase.prototype.displayPosition = function (iframe, config = {}) { const device = STM_Util.navigator.getDeviceType(); const style = { position: 'fixed', }; const width = iframe.style.width; const height = iframe.style.height; const position = config.position ?? this.position; const positionMarginTop = config.positionMarginTop ?? this.positionMarginTop; const positionMarginBottom = config.positionMarginBottom ?? this.positionMarginBottom; const positionMarginLeft = config.positionMarginLeft ?? this.positionMarginLeft; const positionMarginRight = config.positionMarginRight ?? this.positionMarginRight; switch (device) { case 'pc': switch (position) { case '1': // 좌상단 style.top = positionMarginTop; style.left = positionMarginLeft; break; case '2': // 상단 중앙 style.top = positionMarginTop; style.left = `calc(50% - ${width} / 2)`; break; case '3': // 우상단 style.top = positionMarginTop; style.right = positionMarginRight; break; case '4': // 좌중단 style.top = `calc(50% - ${height} / 2)`; style.left = positionMarginLeft; break; case '5': // 중앙 style.top = `calc(50% - ${height} / 2)`; style.left = `calc(50% - ${width} / 2)`; break; case '6': // 우중단 style.top = `calc(50% - ${height} / 2)`; style.right = positionMarginRight; break; case '7': // 좌하단 style.bottom = positionMarginBottom; style.left = positionMarginLeft; break; case '8': // 하단 중앙 style.bottom = positionMarginBottom; style.left = `calc(50% - ${width} / 2)`; break; case '9': // 우하단 style.bottom = positionMarginBottom; style.right = positionMarginRight; break; } break; case 'mo': switch (position) { case '1': // 상단 style.top = positionMarginTop; style.left = '50%'; style.transform = 'translateX(-50%)'; break; case '2': // 중앙 style.top = `calc(50% - ${height} / 2)`; style.left = '50%'; style.transform = 'translateX(-50%)'; break; case '3': // 하단 style.bottom = positionMarginBottom; style.left = '50%'; style.transform = 'translateX(-50%)'; break; } if (this.templateSeq === 6) { delete style.top; style.bottom = '0px'; } else if (this.templateSeq === 14) { style.left = '10px'; delete style.transform; } break; } Object.assign(iframe.style, style); }; CreativeBase.prototype.displayBackground = function (iframe, index) { const id = `snap_onsite_${this.campaignId}_${index}_background`; if (document.getElementById(id)) { return; } this.background = document.createElement('div'); this.background.id = id; this.background.addEventListener('click', this.close.bind(this)); this.background.style.backgroundColor = `rgba(0, 0, 0, ${this.backgroundOpacity})`; this.background.style.position = 'fixed'; this.background.style.top = 0; this.background.style.left = 0; this.background.style.right = 0; this.background.style.bottom = 0; this.background.style.zIndex = BACKGROUND_Z_INDEX; iframe.before(this.background); ScrollLock.lock(); }; CreativeBase.prototype.close = function () { switch (this.type) { case 'product_search': this.triggerIframe.remove(); this.triggerIframe = null; if (this.background) { this.background.remove(); this.background = null; } ScrollLock.unlock(); break; default: this.iframe.style.display = 'none'; if (this.background) { this.background.style.display = 'none'; ScrollLock.unlock(); } break; } }; CreativeBase.prototype.trigger = function (config) { if (this.triggerIframe) { return; } this.triggerIframe = this.createIframe(this.getIframeConfig(1)); this.insertIframe(this.triggerIframe); }; CreativeBase.prototype.delayDisplay = async function () { if (this.displayDelaySecends > 0) { await new Promise(resolve => setTimeout(resolve, this.displayDelaySecends)); } }; CreativeBase.prototype.autoClose = async function () { if (this.autoCloseSeconds > 0) { await new Promise(resolve => setTimeout(resolve, this.autoCloseSeconds)); } this.close(); }; CreativeBase.prototype.getHasInteraction = function () { return this.hasInteraction; }; CreativeBase.prototype.handleInteraction = function (config) { switch (this.interactionType) { case 'scroll': this.handleScrollInteraction(config); break; case 'click': this.handleClickInteraction(config); break; case 'hover': this.handleHoverInteraction(config); break; } }; CreativeBase.prototype.handleScrollInteraction = function (config) { const handleScroll = () => { const scrollTop = window.scrollY || document.documentElement.scrollTop; const docHeight = document.documentElement.scrollHeight - window.innerHeight; const scrolled = (scrollTop / docHeight) * 100; if (scrolled >= this.interactionScrollPercent) { this.display(config); return true; } return false; }; if (handleScroll()) { return; } window.addEventListener('scroll', handleScroll); }; CreativeBase.prototype.handleClickInteraction = function (config) { document.querySelectorAll(this.interactionClickSelector).forEach((element) => { element.addEventListener('click', this.display.bind(this, config)); }); }; CreativeBase.prototype.handleHoverInteraction = function (config) { const device = STM_Util.navigator.getDeviceType(); if (device === 'mo') { this.display(config); } document.querySelectorAll(this.interactionHoverSelector).forEach((element) => { let timer = null; element.addEventListener('mouseenter', () => { timer = setTimeout(() => { this.display(config); }, this.interactionHoverSeconds); }); element.addEventListener('mouseleave', () => { clearTimeout(timer); timer = null; }); }); }; function CreativeEvaluator({ observerManager, onsiteManager }) { /** @type {ObserverManager} */ this.observerManager = observerManager; /** @type {OnsiteManager} */ this.onsiteManager = onsiteManager; this.type = null; this.device = 'none'; // 노출 일 this.displayDayType = null; this.displayDay = null; // 노출 기간 this.displayStartDate = null; this.displayEndDate = null; this.useDisplayEndDate = null; // 노출 시간 this.displayStartTime = null; this.displayEndTime = null; // 노출 페이지: 팝업, 소셜프루프 this.displayPageType = null; this.displayPages = null; // 노출 페이지: 프레임 this.displayPage = null; this.displayPageSelector = null; // 제외 규칙 this.exclusionRules = null; }; CreativeEvaluator.prototype.init = function (type, config) { this.type = type; this.device = config.device ?? 'none'; // 노출 일 this.displayDayType = config.display_type; this.displayDay = config.display_day; // 노출 기간 this.displayStartDate = new Date(config.display_start_date); this.displayEndDate = new Date(config.display_end_date); // 가이드 Rule 6에 따라 String 캐스팅 후 비교 this.useDisplayEndDate = String(config.is_use_end_date) === '1'; // 노출 시간 this.displayStartTime = STM_Util.date.timeToSeconds(config.display_start_time); this.displayEndTime = STM_Util.date.timeToSeconds(config.display_end_time); const setting = config.setting; if (this.type === 'popup' || this.type === 'product_search') { this.displayPageType = setting.display_page_type; this.displayPages = setting.display_page; } else if (this.type === 'frame') { const device = STM_Util.navigator.getDeviceType(); this.displayPage = setting.display_page[device].page; this.displayPageSelector = setting.display_page[device].selector; } if (setting.display_page_detail) { this.displayPageDetail = new RestrictCondition({ observerManager: this.observerManager, onsiteManager: this.onsiteManager }); this.displayPageDetail.init(null, setting.display_page_detail); // todo: 필터 } // 제외 규칙 this.exclusionRules = setting.display_exclusion_rule .split(',') .map((value) => { return value.trim(); }) .filter(Boolean); }; CreativeEvaluator.prototype.evaluate = function (type) { if (type === 'precondition') { return this.evaluatePrecondition(); } else if (type === 'condition') { return this.evaluateCondition(); } return false; }; CreativeEvaluator.prototype.evaluatePrecondition = function () { if (this.type === 'product_search') { if (!this.evaluateDevice()) { return false; } if (!this.evaluateExclusionRule()) { return false; } return true; } if (!this.evaluateDevice()) { return false; } if (!this.evaluateDay()) { return false; } if (!this.evaluateDate()) { return false; } if (!this.evaluateTime()) { return false; } if (!this.evaluateExclusionRule()) { return false; } return true; }; CreativeEvaluator.prototype.evaluateDevice = function () { const device = STM_Util.navigator.getDeviceType(); if (this.device === 'all') { return true; } else { return String(this.device) === String(device); } }; CreativeEvaluator.prototype.evaluateCondition = function () { if (!this.evaluatePage()) { return false; } return true; }; CreativeEvaluator.prototype.evaluateDay = function () { const today = new Date(); switch (this.displayDayType) { case 'week': const days = new Array(7).fill(false); for (let i = 0; i < 7; i++) { days[i] = (this.displayDay & (1 << i)) !== 0; } const todayIndex = (today.getDay() + 6) % 7; if (!days[todayIndex]) { return false; } break; case 'month': // 가이드 Rule 6 적용 if (String(today.getDate()) !== String(this.displayDay)) { return false; } break; } return true; }; CreativeEvaluator.prototype.evaluateDate = function () { const curDate = new Date(); if (curDate < this.displayStartDate) { return false; } if (this.useDisplayEndDate && curDate > this.displayEndDate) { return false; } return true; }; CreativeEvaluator.prototype.evaluateTime = function () { const curDate = new Date(); const curTime = curDate.getHours() * HOUR + curDate.getMinutes() * MINUTE + curDate.getSeconds(); if (this.displayStartTime <= this.displayEndTime) { if (curTime < this.displayStartTime) { return false; } if (curTime > this.displayEndTime) { return false; } } else { // cross-midnight range (e.g. 23:50 ~ 00:50) if (curTime < this.displayStartTime && curTime > this.displayEndTime) { return false; } } return true; }; CreativeEvaluator.prototype.evaluatePage = function () { const pageType = STM_Util.getPageType(); const curPage = PAGE_MAP[pageType]; if ((this.type === 'popup' || this.type === 'product_search') && this.displayPageType === 'custom') { if (this.displayPageDetail) { let isPageIncluded = false; let isCategoryIncluded = false; let isItemIncluded = false; let isPathIncluded = false; isPageIncluded = this.displayPageDetail.inDetail('page', curPage); if (this.displayPages.includes(PAGE_MAP.item_category) && String(curPage) === String(PAGE_MAP.item_category)) { const categoryData = STM_Util.hosting.getCategoryData(location.href); const categoryId = categoryData.categoryNumber; isPageIncluded = false; isCategoryIncluded = this.displayPageDetail.inDetail('category', categoryId); } if (this.displayPages.includes(PAGE_MAP.item_detail) && String(curPage) === String(PAGE_MAP.item_detail)) { const itemData = STM_Util.hosting.getProductData(location.href); const itemId = itemData.productId; isPageIncluded = false; isItemIncluded = this.displayPageDetail.inDetail('item', itemId); } if (this.displayPages.includes('path')) { isPathIncluded = this.displayPageDetail.inDetail('path'); } return isPageIncluded || isCategoryIncluded || isItemIncluded || isPathIncluded; } return this.displayPages.includes(curPage); } else if (this.type === 'frame') { if (!this.displayPageSelector || !document.querySelector(this.displayPageSelector)) { return false; } if (curPage === this.displayPage) { if (this.displayPageDetail) { if (String(curPage) === String(PAGE_MAP.item_category)) { const categoryData = STM_Util.hosting.getCategoryData(location.href); const categoryId = categoryData.categoryNumber; return this.displayPageDetail.inDetail('category', categoryId); } if (String(curPage) === String(PAGE_MAP.item_detail)) { const itemData = STM_Util.hosting.getProductData(location.href); const itemId = itemData.productId; return this.displayPageDetail.inDetail('item', itemId); } } return true; } return String(curPage) === String(this.displayPage); } return true; }; CreativeEvaluator.prototype.evaluateExclusionRule = function () { const href = decodeURIComponent(location.href); const result = this.exclusionRules.some((rule) => { return href.includes(rule); }); return !result; }; ContentBase.TAG = {}; ContentBase.TAG.USER = {}; ContentBase.TAG.USER.LAST_ORDER_DAYS = 'last_order_days'; ContentBase.TAG.USER.LAST_ORDER_PRODUCT = 'last_order_product'; ContentBase.TAG.ITEM = {}; ContentBase.TAG.ITEM.VIEW_1D = 'visit_1d'; ContentBase.TAG.ITEM.VIEW_7D = 'visit_7d'; ContentBase.TAG.ITEM.VIEW_30D = 'visit_30d'; ContentBase.TAG.ITEM.CART_1D = 'cart_1d'; ContentBase.TAG.ITEM.CART_7D = 'cart_7d'; ContentBase.TAG.ITEM.CART_30D = 'cart_30d'; ContentBase.TAG.ITEM.ORDER_1D = 'order_1d'; ContentBase.TAG.ITEM.ORDER_7D = 'order_7d'; ContentBase.TAG.ITEM.ORDER_30D = 'order_30d'; function ContentBase({ userCacheManager, observerManager, onsiteManager, creative }) { /** @type {UserCacheManager} */ this.userCacheManager = userCacheManager; /** @type {ObserverManager} */ this.observerManager = observerManager; /** @type {OnsiteManager} */ this.onsiteManager = onsiteManager; /** @type {CreativeBase} */ this.creative = creative; this.evaluator = new ContentEvalutator(); this.id = null; this.text = ''; this.itemSeq = null; this.couponSeq = null; this.tag = {}; } ContentBase.prototype.init = function (id, config) { this.id = id; this.text = config.text ?? ''; this.itemSeq = config.item_seq; this.couponSeq = config.coupon_seq; this.evaluator.init(config); this.initTag(); this.request(); }; ContentBase.prototype.initTag = function () { const tags = [...Object.values(ContentBase.TAG.USER), ...Object.values(ContentBase.TAG.ITEM)]; for (const tag of tags) { const isIncluded = this.text.includes(`#{${tag}}`); if (isIncluded) { this.tag[tag] = true; } } }; ContentBase.prototype.request = function () { if (stmParams.isMember && (this.tag[ContentBase.TAG.USER.LAST_ORDER_DAYS] || this.tag[ContentBase.TAG.USER.LAST_ORDER_PRODUCT])) { this.onsiteManager.registerRequestData(OnsiteManager.REQUEST_TYPE.CACHE_TYPE, 'buy'); } if (!stmParams.isMember && this.tag[ContentBase.TAG.USER.LAST_ORDER_PRODUCT]) { this.onsiteManager.registerRequestData(OnsiteManager.REQUEST_TYPE.ITEM.NAME, true); } const itemTags = []; for (const tag of Object.values(ContentBase.TAG.ITEM)) { const hasTag = this.tag[tag]; if (hasTag) { itemTags.push(tag); } } if (itemTags.length > 0) { this.onsiteManager.registerRequestData(OnsiteManager.REQUEST_TYPE.ITEM.TAG, { itemSeq: this.itemSeq, tags: itemTags }); } if (this.couponSeq) { this.onsiteManager.registerRequestData(OnsiteManager.REQUEST_TYPE.COUPON, this.couponSeq); } this.observerManager.subscribe(this.response.bind(this)); }; ContentBase.prototype.response = function (type, data) { if (type !== OnsiteManager.EVENT_RESPONSE) { return; } const enableRender = this.evaluateTag(data) && this.evaluateCoupon(data); if (enableRender) { this.registerTag(data); } else { this.creative.removeContent(this.id); } }; ContentBase.prototype.evaluateTag = function (data) { for (const tag of Object.keys(this.tag)) { switch (tag) { case ContentBase.TAG.USER.LAST_ORDER_DAYS: case ContentBase.TAG.USER.LAST_ORDER_PRODUCT: { const history = this.userCacheManager.getDataStorage().get('order'); if (Object.keys(history).length < 1) { return false; } break; } default: { const itemTag = data.itemTag?.[this.itemSeq]; if (itemTag === null || typeof itemTag === 'undefined' || typeof itemTag !== 'object' || Object.keys(itemTag).length < 1) { return false; } const tagValue = itemTag[tag]; if (!tagValue || tagValue === '0') { return false; } break; } } } return true; }; ContentBase.prototype.evaluateCoupon = function (data) { if (!this.couponSeq) { return true; } const coupon = data.coupon?.[this.couponSeq] ?? {}; const state = coupon.use_state === 'Y'; const dateType = coupon.use_date_type; const expiredAt = coupon.use_date_data; let isValid = true; if (dateType === 'data') { isValid = STM_Util.date.getDiffInMs(Date.now(), expiredAt) >= 0; } const isIssueAble = state && isValid; return isIssueAble; }; ContentBase.prototype.getCouponSeq = function () { return this.couponSeq; }; ContentBase.prototype.registerTag = function (data) { for (const tag of Object.keys(this.tag)) { switch (tag) { case ContentBase.TAG.USER.LAST_ORDER_DAYS: if (!stmParams.isMember) { this.registerLastOrderDaysTag(); } break; case ContentBase.TAG.USER.LAST_ORDER_PRODUCT: if (!stmParams.isMember) { this.registerLastOrderProductTag(); } break; default: this.registerItemTag(tag, data); break; } } }; ContentBase.prototype.registerLastOrderDaysTag = function () { const lastOrderDate = this.getLastOrderDate(); const lastOrderDays = STM_Util.date.getDiffInDay(lastOrderDate, Date.now()); this.creative.setTag(ContentBase.TAG.USER.LAST_ORDER_DAYS, lastOrderDays); }; ContentBase.prototype.registerLastOrderProductTag = function () { const history = this.userCacheManager.getDataStorage().get('order'); const lastOrderDate = this.getLastOrderDate(); const lastOrders = []; for (const mainOrder of Object.values(history)) { const orders = mainOrder.orders; const orderDate = orders[0].date; if (String(orderDate) === String(lastOrderDate)) { orders.forEach((order) => { lastOrders.push(order); }); } } const orderProducts = new Set(); for (const order of lastOrders) { const itemId = order.item_id; const price = Number(order.sale_price); orderProducts.add({ itemId, price }); } const stored = Array.from(orderProducts).sort((a, b) => { return b.price - a.price; }); const lastOrderProduct = stmParams.itemName[stored[0].itemId]; this.creative.setTag(ContentBase.TAG.USER.LAST_ORDER_PRODUCT, lastOrderProduct); }; ContentBase.prototype.registerItemTag = function (tag, data) { const tagValue = data.itemTag[this.itemSeq][tag]; const creativeTag = this.creative.getTag(); if (!creativeTag.itemTag) { creativeTag.itemTag = {}; } if (!creativeTag.itemTag[this.itemSeq]) { creativeTag.itemTag[this.itemSeq] = {}; } creativeTag.itemTag[this.itemSeq][tag] = tagValue; }; ContentBase.prototype.getLastOrderDate = function () { const history = this.userCacheManager.getDataStorage().get('order'); const orderDateSet = new Set(); for (const mainOrder of Object.values(history)) { const orderDate = mainOrder.orders[0].date; orderDateSet.add(orderDate); } const sorted = Array.from(orderDateSet).sort((a, b) => { return new Date(b).getTime() - new Date(a).getTime(); }); const lastOrderDate = sorted[0]; return lastOrderDate; }; ContentBase.prototype.evaluate = function (type) { if (!stmParams.isMember && this.text && this.text.includes('#{user_point}')) { return false; } return this.evaluator.evaluate(type); }; function ContentEvalutator() { this.startDate = null; this.endDate = null; this.startTime = null; this.endTime = null; } ContentEvalutator.prototype.init = function (config) { this.startDate = config.start_date; this.endDate = config.end_date; this.startTime = config.start_time; this.endTime = config.end_time; }; ContentEvalutator.prototype.evaluate = function () { if (!this.evaluateDate()) { return false; } if (!this.evaluateTime()) { return false; } return true; }; ContentEvalutator.prototype.evaluateDate = function () { if (!this.startDate && !this.endDate) { return true; } const startDate = new Date(this.startDate); const endDate = new Date(this.endDate); const curDate = new Date(); if (curDate < startDate) { return false; } if (curDate > endDate) { return false; } return true; }; ContentEvalutator.prototype.evaluateTime = function () { if (!this.startTime && !this.endTime) { return true; } const startTime = STM_Util.date.timeToSeconds(this.startTime); const endTime = STM_Util.date.timeToSeconds(this.endTime); const curDate = new Date(); const curTime = curDate.getHours() * HOUR + curDate.getMinutes() * MINUTE + curDate.getSeconds(); if (startTime <= endTime) { if (curTime < startTime) { return false; } if (curTime > endTime) { return false; } } else { // cross-midnight range (e.g. 23:50 ~ 00:50) if (curTime < startTime && curTime > endTime) { return false; } } return true; }; OnsiteUiManager.TOAST_ID = 'onsite_ui_toast'; OnsiteUiManager.TOAST_DURATION = 2500; function OnsiteUiManager() { this.toastEl = null; this.toastTimer = null; this.ensureToastEl(); } OnsiteUiManager.prototype.toast = function (message) { const toast = this.ensureToastEl(); toast.textContent = message; toast.style.opacity = '1'; if (this.toastTimer) { clearTimeout(this.toastTimer); } this.toastTimer = setTimeout(() => { toast.style.opacity = '0'; this.toastTimer = null; }, OnsiteUiManager.TOAST_DURATION); }; OnsiteUiManager.prototype.ensureToastEl = function () { if (!this.toastEl) { const toast = document.createElement('div'); toast.id = OnsiteUiManager.TOAST_ID; Object.assign(toast.style, { position: 'fixed', bottom: '24px', left: '50%', transform: 'translateX(-50%)', padding: '12px 24px', borderRadius: '6px', background: 'rgba(0,0,0,0.75)', fontSize: '14px', color: '#fff', zIndex: '100000002', opacity: '0', transition: 'opacity 0.3s', pointerEvents: 'none', whiteSpace: 'nowrap', }); document.body.appendChild(toast); this.toastEl = toast; } return this.toastEl; }; function OnsiteObserver({ userCacheManager, campaignManager, hostingService }) { /** @type {UserCacheManager} */ this.userCacheManager = userCacheManager; /** @type {CampaignManager} */ this.campaignManager = campaignManager; /** @type {HostingService} */ this.hostingService = hostingService; this.uiManager = new OnsiteUiManager(); this.response = false; this.updateEventPromise = null; } OnsiteObserver.prototype.update = async function (type, data) { switch (type) { case RetryQueue.EVENT_CART: case RetryQueue.EVENT_JOIN: case RetryQueue.EVENT_SEND_COLLECT: await this.userCacheManager.updateEvent(data); break; case UserCacheManager.EVENT_UPDATE: if (data?.data?.ignore_render || data[2]?.ignore_render) { break; } if (this.response === true) { this.renderCampaign(); } break; case MessageEventManager.EVENT: { switch (data.data.type) { case MessageEventManager.EVENT_PREVIEW: this.preview(data.data); break; case MessageEventManager.EVENT_RENDER_READY: this.renderReady(data.data); break; case MessageEventManager.EVENT_CLICK: data.data.ignore_render = true; this.updateEventPromise = this.userCacheManager.updateEvent({ type: 'click', data: data.data }); break; case MessageEventManager.EVENT_LINK: if (this.updateEventPromise) { await this.updateEventPromise; this.updateEventPromise = null; } this.link(data.data); break; case MessageEventManager.EVENT_SIZE: this.size(data.data); break; case MessageEventManager.EVENT_CLOSE: this.close(data.data); break; case MessageEventManager.EVENT_TODAY_NO_SHOW: this.todayNoShow(data.data); break; case MessageEventManager.EVENT_TRIGGER: this.trigger(data.data); break; case MessageEventManager.EVENT_ADD_CART: this.addCart(data.data); break; case MessageEventManager.EVENT_BUY_NOW: this.buyNow(data.data); break; case MessageEventManager.EVENT_TOAST: this.uiManager.toast(data.data.message); break; } break; } case OnsiteManager.EVENT_RESPONSE: this.response = true; break; } }; OnsiteObserver.prototype.renderCampaign = function () { let campaigns = this.getRenderCampaign(); for (let campaign of campaigns) { campaign.render(); } }; OnsiteObserver.prototype.getRenderCampaign = function () { let campaigns = this.campaignManager.getCampaigns(); let groupMap = {}; for (let campaign of Object.values(campaigns)) { if (!campaign.evaluate('condition')) { continue; } let group = campaign.getCreativeType(); if (!groupMap[group]) { groupMap[group] = []; } groupMap[group].push(campaign); } let result = []; for (let group of Object.keys(groupMap)) { const value = groupMap[group]; value.sort((a, b) => { const priorityA = a.getPriorityValue(); const priorityB = b.getPriorityValue(); // 가이드 Rule 6에 따라 비교 시 String 캐스팅 사용 if (String(priorityA) !== String(priorityB)) { return (Number(priorityA) || 0) - (Number(priorityB) || 0); } const dateA = a.getLiveDate(); const dateB = b.getLiveDate(); // Date 객체 유효성 검사 후 타임스탬프 추출 const timeA = (dateA instanceof Date && !isNaN(dateA.getTime())) ? dateA.getTime() : 0; const timeB = (dateB instanceof Date && !isNaN(dateB.getTime())) ? dateB.getTime() : 0; return timeA - timeB; }); switch (group) { case 'popup': { for (const campaign of value) { const isIgnore = campaign.getIsIgnorePriority(); if (isIgnore) { result.push(campaign); } else { result.push(campaign); break; } } break; } case 'frame': { for (const campaign of value) { const isIgnore = campaign.getIsIgnorePriority(); const displayPagePosition = campaign.getCreative().getDisplayPagePosition(); if (displayPagePosition === 'top') { if (isIgnore) { result.unshift(campaign); } else { result.unshift(campaign); break; } } else { if (isIgnore) { result.push(campaign); } else { result.push(campaign); break; } } } break; } case 'product_search': case 'social': { for (const campaign of value) { result.push(campaign); break; } break; } } } result.reverse(); const others = result.filter((campaign) => { return campaign.getCreativeType() !== 'product_search'; }); const searches = result.filter((campaign) => { return campaign.getCreativeType() === 'product_search'; }); return [...searches, ...others]; }; OnsiteObserver.prototype.preview = function (data) { this.campaignManager.renderPreview(data); }; OnsiteObserver.prototype.renderReady = function (data) { const campaign = this.campaignManager.getCampaign(data.campaignId); campaign.display(data); }; OnsiteObserver.prototype.link = function (data) { if (isNaN(Number(data.link))) { location.href = data.link; } else { location.href = STM_Util.hosting.getProductLink(data.link); } }; OnsiteObserver.prototype.size = function (data) { const campaign = this.campaignManager.getCampaign(data.campaignId); campaign.setSize(data.width, data.height); }; OnsiteObserver.prototype.close = function (data) { const campaign = this.campaignManager.getCampaign(data.campaignId); campaign.close(); }; OnsiteObserver.prototype.trigger = function (data) { const campaign = this.campaignManager.getCampaign(data.campaignId); campaign.trigger(); }; OnsiteObserver.prototype.todayNoShow = function (data) { const campaign = this.campaignManager.getCampaign(data.campaignId); campaign.todayNoShow(); }; OnsiteObserver.prototype.addCart = async function (data) { const result = await this.hostingService.addCart(data); if (result === true) { this.uiManager.toast('\uC7A5\uBC14\uAD6C\uB2C8\uC5D0 \uB2F4\uC558\uC2B5\uB2C8\uB2E4.'); }; }; OnsiteObserver.prototype.buyNow = function (data) { this.hostingService.buyNow(data); }; // snaptag run const stm = (typeof SnaptagManager === 'function') ? new SnaptagManager() : null; function run(stm, popstate = false) { if (!stm || typeof stm.run === 'function') { try { stm.run(popstate); } catch (e) { return; } } else { return; } } if (checkCSRPage) { // 샵바이 또는 SPA인 경우 const stmNav = new SnapTagNavigator({ timeoutMs: 2500, }); stmNav.run(); } else { window.addEventListener('load', function () { run(stm); }); } })(window);